Render header strings

This commit is contained in:
Iván Ovejero 2021-11-18 11:32:13 +01:00
parent f1eef04ad2
commit 99963b04a5
25 changed files with 467 additions and 61 deletions

View file

@ -1207,6 +1207,22 @@ class App {
), ),
); );
// Returns node information based on node names and versions
this.app.get(
`/${this.restEndpoint}/node-translation-headers`,
ResponseHelper.send(
async (req: express.Request, res: express.Response): Promise<object | void> => {
const packagesPath = pathJoin(__dirname, '..', '..', '..');
const headersPath = pathJoin(packagesPath, 'nodes-base', 'dist', 'nodes', 'headers');
try {
return require(headersPath);
} catch (error) {
res.status(500).send('Failed to find headers file');
}
},
),
);
// ---------------------------------------- // ----------------------------------------
// Node-Types // Node-Types
// ---------------------------------------- // ----------------------------------------

View file

@ -40,6 +40,7 @@
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"@types/file-saver": "^2.0.1", "@types/file-saver": "^2.0.1",
"@types/jest": "^26.0.13", "@types/jest": "^26.0.13",
"@types/lodash.camelcase": "^4.3.6",
"@types/lodash.get": "^4.4.6", "@types/lodash.get": "^4.4.6",
"@types/lodash.set": "^4.3.6", "@types/lodash.set": "^4.3.6",
"@types/node": "14.17.27", "@types/node": "14.17.27",
@ -69,15 +70,16 @@
"jquery": "^3.4.1", "jquery": "^3.4.1",
"jshint": "^2.9.7", "jshint": "^2.9.7",
"jsplumb": "2.15.4", "jsplumb": "2.15.4",
"lodash.camelcase": "^4.3.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"n8n-workflow": "~0.76.0", "n8n-workflow": "~0.76.0",
"sass": "^1.26.5",
"normalize-wheel": "^1.0.1", "normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1", "prismjs": "^1.17.1",
"quill": "^2.0.0-dev.3", "quill": "^2.0.0-dev.3",
"quill-autoformat": "^0.1.1", "quill-autoformat": "^0.1.1",
"sass": "^1.26.5",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"string-template-parser": "^1.2.6", "string-template-parser": "^1.2.6",
"ts-jest": "^26.3.0", "ts-jest": "^26.3.0",

View file

@ -130,6 +130,7 @@ export interface IRestApi {
getPastExecutions(filter: object, limit: number, lastId?: string | number, firstId?: string | number): Promise<IExecutionsListResponse>; getPastExecutions(filter: object, limit: number, lastId?: string | number, firstId?: string | number): Promise<IExecutionsListResponse>;
stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>; stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>;
makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any
getNodeTranslationHeaders(): Promise<INodeTranslationHeaders>;
getNodeTypes(onlyLatest?: boolean): Promise<INodeTypeDescription[]>; getNodeTypes(onlyLatest?: boolean): Promise<INodeTypeDescription[]>;
getNodesInformation(nodeInfos: INodeTypeNameVersion[]): Promise<INodeTypeDescription[]>; getNodesInformation(nodeInfos: INodeTypeNameVersion[]): Promise<INodeTypeDescription[]>;
getNodeParameterOptions(nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>; getNodeParameterOptions(nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
@ -147,6 +148,15 @@ export interface IRestApi {
getTimezones(): Promise<IDataObject>; getTimezones(): Promise<IDataObject>;
} }
export interface INodeTranslationHeaders {
data: {
[key: string]: {
displayName: string;
description: string;
},
};
}
export interface IBinaryDisplayData { export interface IBinaryDisplayData {
index: number; index: number;
key: string; key: string;

View file

@ -2,7 +2,9 @@
<div :class="$style.container"> <div :class="$style.container">
<el-row> <el-row>
<el-col :span="8" :class="$style.accessLabel"> <el-col :span="8" :class="$style.accessLabel">
<n8n-text :compact="true" :bold="true">{{ $baseText('credentialEdit.credentialInfo.allowUseBy') }}</n8n-text> <n8n-text :compact="true" :bold="true">
{{ $baseText('credentialEdit.credentialInfo.allowUseBy') }}
</n8n-text>
</el-col> </el-col>
<el-col :span="16"> <el-col :span="16">
<div <div
@ -11,7 +13,10 @@
:class="$style.valueLabel" :class="$style.valueLabel"
> >
<el-checkbox <el-checkbox
:label="node.displayName" :label="$headerText({
key: `headers.${shortNodeType(node)}.displayName`,
fallback: node.displayName,
})"
:value="!!nodeAccess[node.name]" :value="!!nodeAccess[node.name]"
@change="(val) => onNodeAccessChange(node.name, val)" @change="(val) => onNodeAccessChange(node.name, val)"
/> />
@ -20,7 +25,9 @@
</el-row> </el-row>
<el-row v-if="currentCredential"> <el-row v-if="currentCredential">
<el-col :span="8" :class="$style.label"> <el-col :span="8" :class="$style.label">
<n8n-text :compact="true" :bold="true">{{ $baseText('credentialEdit.credentialInfo.created') }}</n8n-text> <n8n-text :compact="true" :bold="true">
{{ $baseText('credentialEdit.credentialInfo.created') }}
</n8n-text>
</el-col> </el-col>
<el-col :span="16" :class="$style.valueLabel"> <el-col :span="16" :class="$style.valueLabel">
<n8n-text :compact="true"><TimeAgo :date="currentCredential.createdAt" :capitalize="true" /></n8n-text> <n8n-text :compact="true"><TimeAgo :date="currentCredential.createdAt" :capitalize="true" /></n8n-text>
@ -28,7 +35,9 @@
</el-row> </el-row>
<el-row v-if="currentCredential"> <el-row v-if="currentCredential">
<el-col :span="8" :class="$style.label"> <el-col :span="8" :class="$style.label">
<n8n-text :compact="true" :bold="true">{{ $baseText('credentialEdit.credentialInfo.lastModified') }}</n8n-text> <n8n-text :compact="true" :bold="true">
{{ $baseText('credentialEdit.credentialInfo.lastModified') }}
</n8n-text>
</el-col> </el-col>
<el-col :span="16" :class="$style.valueLabel"> <el-col :span="16" :class="$style.valueLabel">
<n8n-text :compact="true"><TimeAgo :date="currentCredential.updatedAt" :capitalize="true" /></n8n-text> <n8n-text :compact="true"><TimeAgo :date="currentCredential.updatedAt" :capitalize="true" /></n8n-text>
@ -36,7 +45,9 @@
</el-row> </el-row>
<el-row v-if="currentCredential"> <el-row v-if="currentCredential">
<el-col :span="8" :class="$style.label"> <el-col :span="8" :class="$style.label">
<n8n-text :compact="true" :bold="true">{{ $baseText('credentialEdit.credentialInfo.id') }}</n8n-text> <n8n-text :compact="true" :bold="true">
{{ $baseText('credentialEdit.credentialInfo.id') }}
</n8n-text>
</el-col> </el-col>
<el-col :span="16" :class="$style.valueLabel"> <el-col :span="16" :class="$style.valueLabel">
<n8n-text :compact="true">{{ currentCredential.id }}</n8n-text> <n8n-text :compact="true">{{ currentCredential.id }}</n8n-text>
@ -51,6 +62,7 @@ import { renderText } from '../mixins/renderText';
import TimeAgo from '../TimeAgo.vue'; import TimeAgo from '../TimeAgo.vue';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { INodeTypeDescription } from 'n8n-workflow';
export default mixins(renderText).extend({ export default mixins(renderText).extend({
name: 'CredentialInfo', name: 'CredentialInfo',
@ -65,6 +77,9 @@ export default mixins(renderText).extend({
value, value,
}); });
}, },
shortNodeType(nodeType: INodeTypeDescription) {
return nodeType.name.replace('n8n-nodes-base.', '');
},
}, },
}); });
</script> </script>

View file

@ -33,6 +33,15 @@ export default mixins(genericHelpers).extend({
return path.split('.').reduce((acc, part) => acc && acc[part], obj); return path.split('.').reduce((acc, part) => acc && acc[part], obj);
}; };
if (this.keyName === 'name' && this.node.type.startsWith('n8n-nodes-base.')) {
const shortNodeType = this.node.type.replace('n8n-nodes-base.', '');
return this.$headerText({
key: `headers.${shortNodeType}.displayName`,
fallback: getDescendantProp(this.node, this.keyName),
});
}
return getDescendantProp(this.node, this.keyName); return getDescendantProp(this.node, this.keyName);
}, },
}, },

View file

@ -114,7 +114,12 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column property="mode" :label="$baseText('executionsList.mode')" width="100" align="center"></el-table-column> <el-table-column property="mode" :label="$baseText('executionsList.mode')" width="100" align="center">
<!-- TODO i18n <template slot-scope="scope">
{{convertToDisplayDate(scope.row.startedAt)}}<br />
<small v-if="scope.row.id">ID: {{scope.row.id}}</small>
</template> -->
</el-table-column>
<el-table-column :label="$baseText('executionsList.runningTime')" width="150" align="center"> <el-table-column :label="$baseText('executionsList.runningTime')" width="150" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<span v-if="scope.row.stoppedAt === undefined"> <span v-if="scope.row.stoppedAt === undefined">

View file

@ -1,19 +1,31 @@
<template functional> <template>
<div :class="$style.category"> <div :class="$style.category">
<span :class="$style.name">{{ props.item.category }}</span> <span :class="$style.name">
{{ $baseText(`nodeCreator.categoryNames.${categoryName}`) }}
</span>
<font-awesome-icon <font-awesome-icon
:class="$style.arrow" :class="$style.arrow"
icon="chevron-down" icon="chevron-down"
v-if="props.item.properties.expanded" v-if="item.properties.expanded"
/> />
<font-awesome-icon :class="$style.arrow" icon="chevron-up" v-else /> <font-awesome-icon :class="$style.arrow" icon="chevron-up" v-else />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
export default { import Vue from 'vue';
import camelcase from 'lodash.camelcase';
import { renderText } from '@/components/mixins/renderText';
import mixins from 'vue-typed-mixins';
export default mixins(renderText).extend({
props: ['item'], props: ['item'],
}; computed: {
categoryName() {
return camelcase(this.item.category);
},
},
});
</script> </script>

View file

@ -11,9 +11,9 @@
/> />
<div class="type-selector"> <div class="type-selector">
<el-tabs v-model="selectedType" stretch> <el-tabs v-model="selectedType" stretch>
<el-tab-pane label="All" :name="ALL_NODE_FILTER"></el-tab-pane> <el-tab-pane :label="$baseText('nodeCreator.mainPanel.all')" :name="ALL_NODE_FILTER"></el-tab-pane>
<el-tab-pane label="Regular" :name="REGULAR_NODE_FILTER"></el-tab-pane> <el-tab-pane :label="$baseText('nodeCreator.mainPanel.regular')" :name="REGULAR_NODE_FILTER"></el-tab-pane>
<el-tab-pane label="Trigger" :name="TRIGGER_NODE_FILTER"></el-tab-pane> <el-tab-pane :label="$baseText('nodeCreator.mainPanel.trigger')" :name="TRIGGER_NODE_FILTER"></el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
<div v-if="searchFilter.length === 0" class="scrollable"> <div v-if="searchFilter.length === 0" class="scrollable">
@ -55,9 +55,10 @@ import { INodeCreateElement, INodeItemProps, ISubcategoryItemProps } from '@/Int
import { ALL_NODE_FILTER, CORE_NODES_CATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER } from '@/constants'; import { ALL_NODE_FILTER, CORE_NODES_CATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER } from '@/constants';
import SlideTransition from '../transitions/SlideTransition.vue'; import SlideTransition from '../transitions/SlideTransition.vue';
import { matchesNodeType, matchesSelectType } from './helpers'; import { matchesNodeType, matchesSelectType } from './helpers';
import { renderText } from '../mixins/renderText';
export default mixins(externalHooks).extend({ export default mixins(externalHooks, renderText).extend({
name: 'NodeCreateList', name: 'NodeCreateList',
components: { components: {
ItemIterator, ItemIterator,

View file

@ -4,27 +4,31 @@
<NoResultsIcon /> <NoResultsIcon />
</div> </div>
<div class="title"> <div class="title">
<div>We didn't make that... yet</div> <div>
{{ $baseText('nodeCreator.noResults.weDidntMakeThatYet') }}
</div>
<div class="action"> <div class="action">
Dont worry, you can probably do it with the {{ $baseText('nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe') }}
<a @click="selectHttpRequest">HTTP Request</a> or <a @click="selectHttpRequest">{{ $baseText('nodeCreator.noResults.httpRequest') }}</a> or
<a @click="selectWebhook">Webhook</a> node <a @click="selectWebhook">{{ $baseText('nodeCreator.noResults.webhook') }}</a> {{ $baseText('nodeCreator.noResults.node') }}
</div> </div>
</div> </div>
<div class="request"> <div class="request">
<div>Want us to make it faster?</div> <div>
{{ $baseText('nodeCreator.noResults.wantUsToMakeItFaster') }}
</div>
<div> <div>
<a <a
:href="REQUEST_NODE_FORM_URL" :href="REQUEST_NODE_FORM_URL"
target="_blank" target="_blank"
> >
<span>Request the node</span>&nbsp; <span>{{ $baseText('nodeCreator.noResults.requestTheNode') }}</span>&nbsp;
<span> <span>
<font-awesome-icon <font-awesome-icon
class="external" class="external"
icon="external-link-alt" icon="external-link-alt"
title="Request the node" :title="$baseText('nodeCreator.noResults.requestTheNode')"
/> />
</span> </span>
</a> </a>
@ -37,10 +41,11 @@
<script lang="ts"> <script lang="ts">
import { HTTP_REQUEST_NODE_TYPE, REQUEST_NODE_FORM_URL, WEBHOOK_NODE_TYPE } from '@/constants'; import { HTTP_REQUEST_NODE_TYPE, REQUEST_NODE_FORM_URL, WEBHOOK_NODE_TYPE } from '@/constants';
import Vue from 'vue'; import Vue from 'vue';
import { renderText } from '../mixins/renderText';
import mixins from 'vue-typed-mixins';
import NoResultsIcon from './NoResultsIcon.vue'; import NoResultsIcon from './NoResultsIcon.vue';
export default Vue.extend({ export default mixins(renderText).extend({
name: 'NoResults', name: 'NoResults',
components: { components: {
NoResultsIcon, NoResultsIcon,

View file

@ -1,15 +1,25 @@
<template functional> <template>
<div :class="{[$style['node-item']]: true, [$style.bordered]: props.bordered}"> <div :class="{[$style['node-item']]: true, [$style.bordered]: bordered}">
<NodeIcon :class="$style['node-icon']" :nodeType="props.nodeType" /> <NodeIcon :class="$style['node-icon']" :nodeType="nodeType" />
<div> <div>
<div :class="$style.details"> <div :class="$style.details">
<span :class="$style.name">{{props.nodeType.displayName}}</span> <span :class="$style.name">
{{ $headerText({
key: `headers.${shortNodeType}.displayName`,
fallback: nodeType.displayName,
})
}}
</span>
<span :class="$style['trigger-icon']"> <span :class="$style['trigger-icon']">
<TriggerIcon v-if="$options.isTrigger(props.nodeType)" /> <TriggerIcon v-if="$options.isTrigger(nodeType)" />
</span> </span>
</div> </div>
<div :class="$style.description"> <div :class="$style.description">
{{props.nodeType.description}} {{ $headerText({
key: `headers.${shortNodeType}.description`,
fallback: nodeType.description,
})
}}
</div> </div>
</div> </div>
</div> </div>
@ -23,20 +33,30 @@ import { INodeTypeDescription } from 'n8n-workflow';
import NodeIcon from '../NodeIcon.vue'; import NodeIcon from '../NodeIcon.vue';
import TriggerIcon from '../TriggerIcon.vue'; import TriggerIcon from '../TriggerIcon.vue';
import mixins from 'vue-typed-mixins';
import { renderText } from '@/components/mixins/renderText';
Vue.component('NodeIcon', NodeIcon); Vue.component('NodeIcon', NodeIcon);
Vue.component('TriggerIcon', TriggerIcon); Vue.component('TriggerIcon', TriggerIcon);
export default { export default mixins(renderText).extend({
name: 'NodeItem',
props: [ props: [
'active', 'active',
'filter', 'filter',
'nodeType', 'nodeType',
'bordered', 'bordered',
], ],
computed: {
shortNodeType() {
return this.nodeType.name.replace('n8n-nodes-base.', '');
},
},
// @ts-ignore
isTrigger (nodeType: INodeTypeDescription): boolean { isTrigger (nodeType: INodeTypeDescription): boolean {
return nodeType.group.includes('trigger'); return nodeType.group.includes('trigger');
}, },
}; });
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -5,7 +5,7 @@
</div> </div>
<div class="text"> <div class="text">
<input <input
placeholder="Search nodes..." :placeholder="$baseText('nodeCreator.searchBar.searchNodes')"
ref="input" ref="input"
:value="value" :value="value"
@input="onInput" @input="onInput"
@ -22,8 +22,9 @@
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { externalHooks } from '@/components/mixins/externalHooks'; import { externalHooks } from '@/components/mixins/externalHooks';
import { renderText } from '../mixins/renderText';
export default mixins(externalHooks).extend({ export default mixins(externalHooks, renderText).extend({
name: "SearchBar", name: "SearchBar",
props: ["value", "eventBus"], props: ["value", "eventBus"],
mounted() { mounted() {

View file

@ -1,9 +1,11 @@
<template functional> <template>
<div :class="$style.subcategory"> <div :class="$style.subcategory">
<div :class="$style.details"> <div :class="$style.details">
<div :class="$style.title">{{ props.item.properties.subcategory }}</div> <div :class="$style.title">
<div v-if="props.item.properties.description" :class="$style.description"> {{ $baseText(`nodeCreator.subcategoryNames.${subcategoryName}`) }}
{{ props.item.properties.description }} </div>
<div v-if="item.properties.description" :class="$style.description">
{{ $baseText(`nodeCreator.subcategoryDescriptions.${subcategoryDescription}`) }}
</div> </div>
</div> </div>
<div :class="$style.action"> <div :class="$style.action">
@ -13,9 +15,22 @@
</template> </template>
<script lang="ts"> <script lang="ts">
export default { import camelcase from 'lodash.camelcase';
import { renderText } from '@/components/mixins/renderText';
import mixins from 'vue-typed-mixins';
export default mixins(renderText).extend({
props: ['item'], props: ['item'],
}; computed: {
subcategoryName() {
return camelcase(this.item.properties.subcategory);
},
subcategoryDescription() {
const firstWord = this.item.properties.description.split(' ').shift() || '';
return firstWord.toLowerCase().replace(/,/g, '');
},
},
});
</script> </script>

View file

@ -4,7 +4,9 @@
<div class="clickable" @click="onBackArrowClick"> <div class="clickable" @click="onBackArrowClick">
<font-awesome-icon class="back-arrow" icon="arrow-left" /> <font-awesome-icon class="back-arrow" icon="arrow-left" />
</div> </div>
<span>{{ title }}</span> <span>
{{ $baseText(`nodeCreator.subcategoryNames.${subcategoryName}`) }}
</span>
</div> </div>
<div class="scrollable"> <div class="scrollable">
@ -18,17 +20,26 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import camelcase from 'lodash.camelcase';
import { INodeCreateElement } from '@/Interface'; import { INodeCreateElement } from '@/Interface';
import Vue from 'vue'; import Vue from 'vue';
import ItemIterator from './ItemIterator.vue'; import ItemIterator from './ItemIterator.vue';
export default Vue.extend({ import { renderText } from '@/components/mixins/renderText';
import mixins from 'vue-typed-mixins';
export default mixins(renderText).extend({
name: 'SubcategoryPanel', name: 'SubcategoryPanel',
components: { components: {
ItemIterator, ItemIterator,
}, },
props: ['title', 'elements', 'activeIndex'], props: ['title', 'elements', 'activeIndex'],
computed: {
subcategoryName() {
return camelcase(this.title);
},
},
methods: { methods: {
selected(element: INodeCreateElement) { selected(element: INodeCreateElement) {
this.$emit('selected', element); this.$emit('selected', element);

View file

@ -95,9 +95,26 @@ export default mixins(
return null; return null;
}, },
nodeTypeName(): string {
if (this.nodeType) {
const shortNodeType = this.nodeType.name.replace('n8n-nodes-base.', '');
return this.$headerText({
key: `headers.${shortNodeType}.displayName`,
fallback: this.nodeType.name,
});
}
return '';
},
nodeTypeDescription (): string { nodeTypeDescription (): string {
if (this.nodeType && this.nodeType.description) { if (this.nodeType && this.nodeType.description) {
return this.nodeType.description; const shortNodeType = this.nodeType.name.replace('n8n-nodes-base.', '');
return this.$headerText({
key: `headers.${shortNodeType}.description`,
fallback: this.nodeType.description,
});
} else { } else {
return this.$baseText('nodeSettings.noDescriptionFound'); return this.$baseText('nodeSettings.noDescriptionFound');
} }

View file

@ -19,10 +19,19 @@ export const renderText = Vue.extend({
}, },
/** /**
* 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. * 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);
},
/**
* 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.
*/ */
__render( __render(
{ key, fallback }: { key: string, fallback: string }, { key, fallback }: { key: string; fallback: string; },
) { ) {
return this.$te(key) ? this.$t(key).toString() : fallback; return this.$te(key) ? this.$t(key).toString() : fallback;
}, },

View file

@ -16,6 +16,7 @@ import {
IWorkflowShortResponse, IWorkflowShortResponse,
IRestApi, IRestApi,
IWorkflowDataUpdate, IWorkflowDataUpdate,
INodeTranslationHeaders,
} from '@/Interface'; } from '@/Interface';
import { import {
IDataObject, IDataObject,
@ -78,6 +79,10 @@ export const restApi = Vue.extend({
return self.restApi().makeRestApiRequest('POST', `/executions-current/${executionId}/stop`); return self.restApi().makeRestApiRequest('POST', `/executions-current/${executionId}/stop`);
}, },
getNodeTranslationHeaders: (): Promise<INodeTranslationHeaders> => {
return self.restApi().makeRestApiRequest('GET', '/node-translation-headers');
},
// Returns all node-types // Returns all node-types
getNodeTypes: (onlyLatest = false): Promise<INodeTypeDescription[]> => { getNodeTypes: (onlyLatest = false): Promise<INodeTypeDescription[]> => {
return self.restApi().makeRestApiRequest('GET', `/node-types`, {onlyLatest}); return self.restApi().makeRestApiRequest('GET', `/node-types`, {onlyLatest});

View file

@ -71,7 +71,7 @@ export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
export const SUBCATEGORY_DESCRIPTIONS: { export const SUBCATEGORY_DESCRIPTIONS: {
[category: string]: { [subcategory: string]: string }; [category: string]: { [subcategory: string]: string };
} = { } = {
'Core Nodes': { 'Core Nodes': { // this - all subkeys are set from codex
Flow: 'Branches, core triggers, merge data', Flow: 'Branches, core triggers, merge data',
Files: 'Work with CSV, XML, text, images etc.', Files: 'Work with CSV, XML, text, images etc.',
'Data Transformation': 'Manipulate data fields, run code', 'Data Transformation': 'Manipulate data fields, run code',

View file

@ -2,7 +2,7 @@ import Vue from 'vue';
import VueI18n from 'vue-i18n'; import VueI18n from 'vue-i18n';
import englishBaseText from './locales/en'; import englishBaseText from './locales/en';
import axios from 'axios'; import axios from 'axios';
import path from 'path'; import { INodeTranslationHeaders } from '@/Interface';
Vue.use(VueI18n); Vue.use(VueI18n);
@ -27,6 +27,7 @@ function setLanguage(language: string) {
} }
export async function loadLanguage(language?: string) { export async function loadLanguage(language?: string) {
// TODO i18n: Remove next line
console.log(`loadLanguage called with ${language}`); // eslint-disable-line no-console console.log(`loadLanguage called with ${language}`); // eslint-disable-line no-console
if (!language) return Promise.resolve(); if (!language) return Promise.resolve();
@ -39,11 +40,11 @@ export async function loadLanguage(language?: string) {
return Promise.resolve(setLanguage(language)); return Promise.resolve(setLanguage(language));
} }
const baseText = require(`./locales/${language}`).default; // TODO i18n: `path.join()` const baseText = require(`./locales/${language}`).default;
i18n.setLocaleMessage(language, baseText); i18n.setLocaleMessage(language, baseText);
loadedLanguages.push(language); loadedLanguages.push(language);
return setLanguage(language); setLanguage(language);
} }
export function addNodeTranslation( export function addNodeTranslation(
@ -65,3 +66,16 @@ export function addNodeTranslation(
Object.assign(i18n.messages[language], newNodesBase), Object.assign(i18n.messages[language], newNodesBase),
); );
} }
export function addHeaders(
headers: INodeTranslationHeaders,
language: string,
) {
i18n.setLocaleMessage(
language,
Object.assign(i18n.messages[language], { headers }),
);
// TODO i18n: Remove next line
console.log(i18n.messages.de.headers); // eslint-disable-line no-console
}

View file

@ -5,6 +5,52 @@ export default {
clientSecret: '🇩🇪 Client Secret', clientSecret: '🇩🇪 Client Secret',
}, },
}, },
nodeCreator: {
categoryNames: {
coreNodes: '🇩🇪 Core Nodes',
customNodes: '🇩🇪 Custom Nodes',
suggestedNodes: '🇩🇪 Suggested Nodes ✨',
analytics: '🇩🇪 Analytics',
communication: '🇩🇪 Communication',
dataStorage: '🇩🇪 Data & Storage',
development: '🇩🇪 Development',
financeAccounting: '🇩🇪 Finance & Accounting',
marketingContent: '🇩🇪 Marketing & Content',
productivity: '🇩🇪 Productivity',
sales: '🇩🇪 Sales',
utility: '🇩🇪 Utility',
miscellaneous: '🇩🇪 Miscellaneous',
},
subcategoryNames: {
dataTransformation: '🇩🇪 Data Transformation',
flow: '🇩🇪 Flow',
files: '🇩🇪 Files',
helpers: '🇩🇪 Helpers',
},
subcategoryDescriptions: {
manipulate: '🇩🇪 Manipulate data fields, run code',
branches: '🇩🇪 Branches, core triggers, merge data',
work: '🇩🇪 Work with CSV, XML, text, images etc.',
http: '🇩🇪 HTTP Requests (API calls), date and time, scrape HTML',
},
mainPanel: {
all: '🇩🇪 All',
regular: '🇩🇪 Regular',
trigger: '🇩🇪 Trigger',
},
searchBar: {
searchNodes: '🇩🇪 Search nodes...',
},
noResults: {
weDidntMakeThatYet: "🇩🇪 We didn't make that... yet",
dontWorryYouCanProbablyDoItWithThe: '🇩🇪 Dont worry, you can probably do it with the {httpRequest} or {webhook} node',
httpRequest: '🇩🇪 HTTP Request',
webhook: '🇩🇪 Webhook',
node: '🇩🇪 node',
wantUsToMakeItFaster: '🇩🇪 Want us to make it faster?',
requestTheNode: '🇩🇪 Request the node',
},
},
textEdit: { textEdit: {
edit: '🇩🇪 Edit', edit: '🇩🇪 Edit',
}, },

View file

@ -1,4 +1,50 @@
export default { export default {
nodeCreator: {
categoryNames: {
coreNodes: 'Core Nodes',
customNodes: 'Custom Nodes',
suggestedNodes: 'Suggested Nodes ✨',
analytics: 'Analytics',
communication: 'Communication',
dataStorage: 'Data & Storage',
development: 'Development',
financeAccounting: 'Finance & Accounting',
marketingContent: 'Marketing & Content',
productivity: 'Productivity',
sales: 'Sales',
utility: 'Utility',
miscellaneous: 'Miscellaneous',
},
subcategoryNames: {
dataTransformation: 'Data Transformation',
flow: 'Flow',
files: 'Files',
helpers: 'Helpers',
},
subcategoryDescriptions: {
manipulate: 'Manipulate data fields, run code',
branches: 'Branches, core triggers, merge data',
work: 'Work with CSV, XML, text, images etc.',
http: 'HTTP Requests (API calls), date and time, scrape HTML',
},
mainPanel: {
all: 'All',
regular: 'Regular',
trigger: 'Trigger',
},
searchBar: {
searchNodes: 'Search nodes...',
},
noResults: {
weDidntMakeThatYet: "We didn't make that... yet",
dontWorryYouCanProbablyDoItWithThe: 'Dont worry, you can probably do it with the {httpRequest} or {webhook} node',
httpRequest: 'HTTP Request',
webhook: 'Webhook',
node: 'node',
wantUsToMakeItFaster: 'Want us to make it faster?',
requestTheNode: 'Request the node',
},
},
textEdit: { textEdit: {
edit: 'Edit', edit: 'Edit',
}, },

View file

@ -170,7 +170,11 @@ import {
IExecutionsSummary, IExecutionsSummary,
} from '../Interface'; } from '../Interface';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { loadLanguage, addNodeTranslation } from '@/i18n'; import {
loadLanguage,
addNodeTranslation,
addHeaders,
} from '@/i18n';
const NODE_SIZE = 100; const NODE_SIZE = 100;
const DEFAULT_START_POSITION_X = 250; const DEFAULT_START_POSITION_X = 250;
@ -245,9 +249,13 @@ export default mixins(
deep: true, deep: true,
}, },
defaultLocale (newLocale, oldLocale) { async defaultLocale (newLocale, oldLocale) {
// TODO i18n: Remove next line
console.log(`Switching locale from ${oldLocale} to ${newLocale}`); // eslint-disable-line no-console console.log(`Switching locale from ${oldLocale} to ${newLocale}`); // eslint-disable-line no-console
loadLanguage(newLocale); loadLanguage(newLocale);
const headers = await this.restApi().getNodeTranslationHeaders();
addHeaders(headers, this.$store.getters.defaultLocale);
}, },
}, },
async beforeRouteLeave(to, from, next) { async beforeRouteLeave(to, from, next) {

View file

@ -1,11 +1,141 @@
const { src, dest } = require('gulp'); const { existsSync, promises: { writeFile } } = require('fs');
const path = require('path');
const { task, src, dest } = require('gulp');
const ALLOWED_HEADER_KEYS = ['displayName', 'description'];
const PURPLE_ANSI_COLOR_CODE = 35;
task('build:icons', copyIcons);
function copyIcons() { function copyIcons() {
src('nodes/**/*.{png,svg}') src('nodes/**/*.{png,svg}').pipe(dest('dist/nodes'))
.pipe(dest('dist/nodes'))
return src('credentials/**/*.{png,svg}') return src('credentials/**/*.{png,svg}').pipe(dest('dist/credentials'));
.pipe(dest('dist/credentials'));
} }
exports.default = copyIcons; task('build:translations', writeHeadersAndTranslations);
/**
* Write all node translation headers at `/dist/nodes/headers.js` and write
* each node translation at `/dist/nodes/<node>/translations/<language>.js`
*/
function writeHeadersAndTranslations(done) {
checkLocale();
const paths = getTranslationPaths();
const { headers, translations } = getHeadersAndTranslations(paths);
const headersDestinationPath = path.join(__dirname, 'dist', 'nodes', 'headers.js');
writeDestinationFile(headersDestinationPath, headers);
log('Headers translation file written to:');
log(headersDestinationPath, { bulletpoint: true });
translations.forEach(t => {
writeDestinationFile(t.destinationPath, t.content);
});
log('Main translation files written to:');
translations.forEach(t => log(t.destinationPath, { bulletpoint: true }))
done();
}
function getTranslationPaths() {
const destinationPaths = require('./package.json').n8n.nodes;
const { N8N_DEFAULT_LOCALE: locale } = process.env;
const seen = {};
return destinationPaths.reduce((acc, cur) => {
const sourcePath = path.join(
__dirname,
cur.split('/').slice(1, -1).join('/'),
'translations',
`${locale}.ts`,
);
if (existsSync(sourcePath) && !seen[sourcePath]) {
seen[sourcePath] = true;
const destinationPath = path.join(
__dirname,
cur.split('/').slice(0, -1).join('/'),
'translations',
`${locale}.js`,
);
acc.push({
source: sourcePath,
destination: destinationPath,
});
};
return acc;
}, []);
}
function getHeadersAndTranslations(paths) {
return paths.reduce((acc, cur) => {
const translation = require(cur.source);
const nodeType = Object.keys(translation).pop();
const { header } = translation[nodeType];
if (isValidHeader(header, ALLOWED_HEADER_KEYS)) {
acc.headers[nodeType] = header;
}
acc.translations.push({
destinationPath: cur.destination,
content: translation,
});
return acc;
}, { headers: {}, translations: [] });
}
// ----------------------------------
// helpers
// ----------------------------------
function isValidHeader(header, allowedHeaderKeys) {
if (!header) return false;
const headerKeys = Object.keys(header);
return headerKeys.length > 0 &&
headerKeys.every(key => allowedHeaderKeys.includes(key));
}
function checkLocale() {
const { N8N_DEFAULT_LOCALE: locale } = process.env;
log(`Default locale set to: ${colorize(PURPLE_ANSI_COLOR_CODE, locale || 'en')}`);
if (!locale || locale === 'en') {
log('No translation required - Skipping translations build...');
return;
};
}
function writeDestinationFile(destinationPath, data) {
writeFile(
destinationPath,
`module.exports = ${JSON.stringify(data, null, 2)}`,
);
}
const log = (string, { bulletpoint } = { bulletpoint: false }) => {
if (bulletpoint) {
process.stdout.write(
colorize(PURPLE_ANSI_COLOR_CODE, `- ${string}\n`),
);
return;
};
process.stdout.write(`${string}\n`);
};
const colorize = (ansiColorCode, string) =>
['\033[', ansiColorCode, 'm', string, '\033[0m'].join('')

View file

@ -1,5 +1,9 @@
module.exports = { module.exports = {
bitwarden: { bitwarden: {
header: {
displayName: '🇩🇪 Bitwarden',
description: '🇩🇪 Consume Bitwarden API',
},
credentialsModal: { credentialsModal: {
bitwardenApi: { bitwardenApi: {
environment: { environment: {

View file

@ -1,5 +1,9 @@
module.exports = { module.exports = {
github: { github: {
header: {
displayName: '🇩🇪 GitHub',
description: '🇩🇪 Consume GitHub API',
},
credentialsModal: { credentialsModal: {
githubOAuth2Api: { githubOAuth2Api: {
server: { server: {

View file

@ -16,7 +16,8 @@
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
"scripts": { "scripts": {
"dev": "npm run watch", "dev": "npm run watch",
"build": "tsc && gulp", "build": "tsc && gulp build:icons && gulp build:translations",
"build:translations": "gulp build:translations",
"format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/nodes-base/**/**.ts --write", "format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/nodes-base/**/**.ts --write",
"lint": "tslint -p tsconfig.json -c tslint.json", "lint": "tslint -p tsconfig.json -c tslint.json",
"lintfix": "tslint --fix -p tsconfig.json -c tslint.json", "lintfix": "tslint --fix -p tsconfig.json -c tslint.json",