diff --git a/.gitignore b/.gitignore index a0b76f532b..94e1dcdc9f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,5 @@ _START_PACKAGE .vscode/* !.vscode/extensions.json .idea -vetur.config.js nodelinter.config.json packages/*/package-lock.json diff --git a/package-lock.json b/package-lock.json index f618a322c2..6d2bc54f2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12523,6 +12523,14 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==" }, + "@types/lodash.camelcase": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/lodash.camelcase/-/lodash.camelcase-4.3.6.tgz", + "integrity": "sha512-hd/TEuPd76Jtf1xEq85CHbCqR+iqvs5IOKyrYbiaOg69BRQgPN9XkvLj8Jl8rBp/dfJ2wQ1AVcP8mZmybq7kIg==", + "requires": { + "@types/lodash": "*" + } + }, "@types/lodash.get": { "version": "4.4.6", "resolved": "https://registry.npmjs.org/@types/lodash.get/-/lodash.get-4.4.6.tgz", @@ -31861,6 +31869,11 @@ "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", "dev": true }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -42650,6 +42663,11 @@ "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==" }, + "vue-i18n": { + "version": "8.26.7", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.26.7.tgz", + "integrity": "sha512-7apa5PvRg1YCLoraE3lOgpCG8hJGupLCtywQWedWsgBbvF0TOgFvhitqK9xRH0PBGG1G8aiJz9oklyNDFfDxLg==" + }, "vue-inbrowser-compiler-utils": { "version": "4.43.0", "resolved": "https://registry.npmjs.org/vue-inbrowser-compiler-utils/-/vue-inbrowser-compiler-utils-4.43.0.tgz", diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index c4b822ffa8..c06c1e200e 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -689,6 +689,13 @@ const config = convict({ }, }, }, + + defaultLocale: { + doc: 'Default locale for the UI', + format: String, + default: 'en', + env: 'N8N_DEFAULT_LOCALE', + }, }); // Overwrite default configuration with settings which got defined in diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 8559dd4968..ae977cbcde 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -403,6 +403,7 @@ export interface IN8nUISettings { instanceId: string; telemetry: ITelemetrySettings; personalizationSurvey: IPersonalizationSurvey; + defaultLocale: string; } export interface IPersonalizationSurveyAnswers { diff --git a/packages/cli/src/NodeTypes.ts b/packages/cli/src/NodeTypes.ts index ff4e8c027c..046123927b 100644 --- a/packages/cli/src/NodeTypes.ts +++ b/packages/cli/src/NodeTypes.ts @@ -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,29 @@ class NodeTypesClass implements INodeTypes { return this.nodeTypes[nodeType].type; } + /** + * Variant of `getByNameAndVersion` that includes the node's source path, used to locate a node's translations. + */ + getWithSourcePath( + 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); } } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 2a647abaa7..dbe60af67f 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -24,8 +24,12 @@ /* 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 */ +/* eslint-disable no-await-in-loop */ + import * as express from 'express'; -import { readFileSync } from 'fs'; +import { readFileSync, existsSync } from 'fs'; +import { readFile } from 'fs/promises'; 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 +148,7 @@ import { InternalHooksManager } from './InternalHooksManager'; import { TagEntity } from './databases/entities/TagEntity'; import { WorkflowEntity } from './databases/entities/WorkflowEntity'; import { NameRequest } from './WorkflowHelpers'; +import { getNodeTranslationPath } from './TranslationHelpers'; require('body-parser-xml')(bodyParser); @@ -280,6 +285,7 @@ class App { personalizationSurvey: { shouldShow: false, }, + defaultLocale: config.get('defaultLocale'), }; } @@ -1151,13 +1157,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); @@ -1176,17 +1182,60 @@ class App { ResponseHelper.send( async (req: express.Request, res: express.Response): Promise => { 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 { defaultLocale } = this.frontendSettings; + + if (defaultLocale === 'en') { + return nodeInfos.reduce((acc, { name, version }) => { + const { description } = NodeTypes().getByNameAndVersion(name, version); + acc.push(description); + return acc; + }, []); + } + + async function populateTranslation( + name: string, + version: number, + nodeTypes: INodeTypeDescription[], + ) { + const { description, sourcePath } = NodeTypes().getWithSourcePath(name, version); + const translationPath = await getNodeTranslationPath(sourcePath, defaultLocale); + + try { + const translation = await readFile(translationPath, 'utf8'); + description.translation = JSON.parse(translation); + } catch (error) { + // ignore - no translation at expected translation path } - }); - return returnData; + nodeTypes.push(description); + } + + const nodeTypes: INodeTypeDescription[] = []; + + const promises = nodeInfos.map(async ({ name, version }) => + populateTranslation(name, version, nodeTypes), + ); + + await Promise.all(promises); + + return nodeTypes; + }, + ), + ); + + // 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 => { + 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'); + } }, ), ); @@ -1584,6 +1633,7 @@ class App { // No idea if multiple where parameters make db search // slower but to be sure that that is not the case we // remove all unnecessary fields in case the id is defined. + // @ts-ignore findQuery.where = { id: findQuery.where.id }; } } @@ -2859,6 +2909,12 @@ export async function start(): Promise { console.log(`n8n ready on ${ADDRESS}, port ${PORT}`); console.log(`Version: ${versions.cli}`); + const defaultLocale = config.get('defaultLocale'); + + if (defaultLocale !== 'en') { + console.log(`Locale: ${defaultLocale}`); + } + await app.externalHooks.run('n8n.ready', [app]); const cpus = os.cpus(); const diagnosticInfo: IDiagnosticInfo = { diff --git a/packages/cli/src/TranslationHelpers.ts b/packages/cli/src/TranslationHelpers.ts new file mode 100644 index 0000000000..547c3e9c30 --- /dev/null +++ b/packages/cli/src/TranslationHelpers.ts @@ -0,0 +1,39 @@ +import { join, dirname } from 'path'; +import { readdir } from 'fs/promises'; +import { Dirent } from 'fs'; + +const ALLOWED_VERSIONED_DIRNAME_LENGTH = [2, 3]; // v1, v10 + +function isVersionedDirname(dirent: Dirent) { + if (!dirent.isDirectory()) return false; + + return ( + ALLOWED_VERSIONED_DIRNAME_LENGTH.includes(dirent.name.length) && + dirent.name.toLowerCase().startsWith('v') + ); +} + +async function getMaxVersion(from: string) { + const entries = await readdir(from, { withFileTypes: true }); + + const dirnames = entries.reduce((acc, cur) => { + if (isVersionedDirname(cur)) acc.push(cur.name); + return acc; + }, []); + + if (!dirnames.length) return null; + + return Math.max(...dirnames.map((d) => parseInt(d.charAt(1), 10))); +} + +export async function getNodeTranslationPath( + nodeSourcePath: string, + language: string, +): Promise { + const nodeDir = dirname(nodeSourcePath); + const maxVersion = await getMaxVersion(nodeDir); + + return maxVersion + ? join(nodeDir, `v${maxVersion}`, 'translations', `${language}.json`) + : join(nodeDir, 'translations', `${language}.json`); +} diff --git a/packages/core/test/Helpers.ts b/packages/core/test/Helpers.ts index 4eb58e5578..7335eb0d5f 100644 --- a/packages/core/test/Helpers.ts +++ b/packages/core/test/Helpers.ts @@ -723,7 +723,7 @@ class NodeTypesClass implements INodeTypes { async init(nodeTypes: INodeTypeData): Promise {} 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 { @@ -731,7 +731,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); } } diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index a109e5831c..84028aa5c2 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -29,7 +29,8 @@ "n8n-design-system": "~0.9.0", "timeago.js": "^4.0.2", "v-click-outside": "^3.1.2", - "vue-fragment": "^1.5.2" + "vue-fragment": "^1.5.2", + "vue-i18n": "^8.26.7" }, "devDependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.35", @@ -39,6 +40,7 @@ "@types/express": "^4.17.6", "@types/file-saver": "^2.0.1", "@types/jest": "^26.0.13", + "@types/lodash.camelcase": "^4.3.6", "@types/lodash.get": "^4.4.6", "@types/lodash.set": "^4.3.6", "@types/node": "14.17.27", @@ -68,15 +70,16 @@ "jquery": "^3.4.1", "jshint": "^2.9.7", "jsplumb": "2.15.4", + "lodash.camelcase": "^4.3.0", "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", "n8n-workflow": "~0.79.0", - "sass": "^1.26.5", "normalize-wheel": "^1.0.1", "prismjs": "^1.17.1", "quill": "^2.0.0-dev.3", "quill-autoformat": "^0.1.1", + "sass": "^1.26.5", "sass-loader": "^8.0.2", "string-template-parser": "^1.2.6", "ts-jest": "^26.3.0", diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 4685b8d2aa..a0ab00a93b 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -163,6 +163,7 @@ export interface IRestApi { getPastExecutions(filter: object, limit: number, lastId?: string | number, firstId?: string | number): Promise; stopCurrentExecution(executionId: string): Promise; makeRestApiRequest(method: string, endpoint: string, data?: any): Promise; // tslint:disable-line:no-any + getNodeTranslationHeaders(): Promise; getNodeTypes(onlyLatest?: boolean): Promise; getNodesInformation(nodeInfos: INodeTypeNameVersion[]): Promise; getNodeParameterOptions(nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise; @@ -180,6 +181,15 @@ export interface IRestApi { getTimezones(): Promise; } +export interface INodeTranslationHeaders { + data: { + [key: string]: { + displayName: string; + description: string; + }, + }; +} + export interface IBinaryDisplayData { index: number; key: string; @@ -523,6 +533,7 @@ export interface IN8nUISettings { instanceId: string; personalizationSurvey?: IPersonalizationSurvey; telemetry: ITelemetrySettings; + defaultLocale: string; } export interface IWorkflowSettings extends IWorkflowSettingsWorkflow { @@ -636,6 +647,8 @@ export interface IRootState { activeActions: string[]; activeNode: string | null; baseUrl: string; + credentialTextRenderKeys: { nodeType: string; credentialType: string; } | null; + defaultLocale: string; endpointWebhook: string; endpointWebhookTest: string; executionId: string | null; diff --git a/packages/editor-ui/src/components/About.vue b/packages/editor-ui/src/components/About.vue index b2c079dbf2..fabe87571f 100644 --- a/packages/editor-ui/src/components/About.vue +++ b/packages/editor-ui/src/components/About.vue @@ -1,18 +1,18 @@