Skip to content

Commit 1de9ecf

Browse files
committed
✨ Set up i18n
1 parent a46c7f8 commit 1de9ecf

19 files changed

+1891
-13
lines changed

.gitignore

-3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,5 @@ yarn.lock
1010
google-generated-credentials.json
1111
_START_PACKAGE
1212
.env
13-
.vscode/*
14-
!.vscode/extensions.json
1513
.idea
16-
vetur.config.js
1714
nodelinter.config.json

.vscode/settings.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"vetur.experimental.templateInterpolationService": true,
3+
}

packages/cli/config/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,13 @@ const config = convict({
689689
},
690690
},
691691
},
692+
693+
defaultLocale: {
694+
doc: 'Default locale for the UI',
695+
format: String,
696+
default: 'en',
697+
env: 'N8N_DEFAULT_LOCALE',
698+
},
692699
});
693700

694701
// Overwrite default configuration with settings which got defined in

packages/cli/src/Interfaces.ts

+1
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ export interface IN8nUISettings {
394394
instanceId: string;
395395
telemetry: ITelemetrySettings;
396396
personalizationSurvey: IPersonalizationSurvey;
397+
defaultLocale: string;
397398
}
398399

399400
export interface IPersonalizationSurveyAnswers {

packages/cli/src/Server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ class App {
280280
personalizationSurvey: {
281281
shouldShow: false,
282282
},
283+
defaultLocale: config.get('defaultLocale'),
283284
};
284285
}
285286

packages/editor-ui/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"n8n-design-system": "~0.6.0",
3030
"timeago.js": "^4.0.2",
3131
"v-click-outside": "^3.1.2",
32-
"vue-fragment": "^1.5.2"
32+
"vue-fragment": "^1.5.2",
33+
"vue-i18n": "^8.26.7"
3334
},
3435
"devDependencies": {
3536
"@fortawesome/fontawesome-svg-core": "^1.2.35",

packages/editor-ui/src/Interface.ts

+2
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ export interface IN8nUISettings {
470470
instanceId: string;
471471
personalizationSurvey?: IPersonalizationSurvey;
472472
telemetry: ITelemetrySettings;
473+
defaultLocale: string;
473474
}
474475

475476
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
@@ -583,6 +584,7 @@ export interface IRootState {
583584
activeActions: string[];
584585
activeNode: string | null;
585586
baseUrl: string;
587+
defaultLocale: string;
586588
endpointWebhook: string;
587589
endpointWebhookTest: string;
588590
executionId: string | null;

packages/editor-ui/src/components/About.vue

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,36 @@
11
<template>
22
<span>
3-
<el-dialog class="n8n-about" :visible="dialogVisible" append-to-body width="50%" title="About n8n" :before-close="closeDialog">
3+
<el-dialog class="n8n-about" :visible="dialogVisible" append-to-body width="50%" :title="$baseText('about.aboutN8n')" :before-close="closeDialog">
44
<div>
55
<el-row>
66
<el-col :span="8" class="info-name">
7-
n8n Version:
7+
{{ $baseText('about.n8nVersion') }}
88
</el-col>
99
<el-col :span="16">
10-
{{versionCli}}
10+
{{ versionCli }}
1111
</el-col>
1212
</el-row>
1313
<el-row>
1414
<el-col :span="8" class="info-name">
15-
Source Code:
15+
{{ $baseText('about.sourceCode') }}
1616
</el-col>
1717
<el-col :span="16">
1818
<a href="https://github.com/n8n-io/n8n" target="_blank">https://github.com/n8n-io/n8n</a>
1919
</el-col>
2020
</el-row>
2121
<el-row>
2222
<el-col :span="8" class="info-name">
23-
License:
23+
{{ $baseText('about.license') }}
2424
</el-col>
2525
<el-col :span="16">
26-
<a href="https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md" target="_blank">Apache 2.0 with Commons Clause</a>
26+
<a href="https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md" target="_blank">
27+
{{ $baseText('about.apacheWithCommons20Clause') }}
28+
</a>
2729
</el-col>
2830
</el-row>
2931

3032
<div class="action-buttons">
31-
<n8n-button @click="closeDialog" label="Close" />
33+
<n8n-button @click="closeDialog" :label="$baseText('about.close')" />
3234
</div>
3335
</div>
3436
</el-dialog>

packages/editor-ui/src/components/mixins/genericHelpers.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { showMessage } from '@/components/mixins/showMessage';
2+
import { translate } from '@/components/mixins/translate';
23
import { debounce } from 'lodash';
34

45
import mixins from 'vue-typed-mixins';
56

6-
export const genericHelpers = mixins(showMessage).extend({
7+
export const genericHelpers = mixins(showMessage, translate).extend({
78
data () {
89
return {
910
loadingService: null as any | null, // tslint:disable-line:no-any
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// import { TranslationPath } from '@/Interface';
2+
import Vue from 'vue';
3+
4+
/**
5+
* Mixin to translate:
6+
* - base strings, i.e. any string that is not node- or credentials-specific,
7+
* - specific strings,
8+
* - node-specific strings, i.e. those in `NodeView.vue`,
9+
* - credentials-specific strings, i.e. those in `EditCredentials.vue`.
10+
*/
11+
export const translate = Vue.extend({
12+
computed: {
13+
/**
14+
* Node type for the active node in `NodeView.vue`.
15+
*/
16+
activeNodeType (): string {
17+
return this.$store.getters.activeNode.type;
18+
},
19+
},
20+
21+
methods: {
22+
// -----------------------------------------
23+
// main methods
24+
// -----------------------------------------
25+
26+
/**
27+
* Translate a base string. Called directly in Vue templates.
28+
* Optionally, [interpolate a variable](https://kazupon.github.io/vue-i18n/guide/formatting.html#named-formatting).
29+
*/
30+
$baseText(
31+
key: string,
32+
options?: { interpolate?: { [key: string]: string } },
33+
): string {
34+
const translatedBaseString = options && options.interpolate
35+
? this.$t(key, options.interpolate)
36+
: this.$t(key);
37+
38+
return translatedBaseString.toString();
39+
},
40+
41+
/**
42+
* Translate a node- or credentials-specific string.
43+
* Called in-mixin by node- or credentials-specific methods,
44+
* which are called directly in Vue templates.
45+
*/
46+
translateSpecific(
47+
{ key, fallback }: { key: string, fallback: string },
48+
): string {
49+
return this.$te(key) ? this.$t(key).toString() : fallback;
50+
},
51+
52+
// -----------------------------------------
53+
// node-specific methods
54+
// -----------------------------------------
55+
56+
/**
57+
* Translate a top-level node parameter name, i.e. leftmost parameter in `NodeView.vue`.
58+
*/
59+
$translateNodeParameterName(
60+
{ name: parameterName, displayName }: { name: string; displayName: string; },
61+
) {
62+
return this.translateSpecific({
63+
key: `${this.activeNodeType}.parameters.${parameterName}.displayName`,
64+
fallback: displayName,
65+
});
66+
},
67+
68+
/**
69+
* Translate a top-level parameter description for a node or for credentials.
70+
*/
71+
$translateDescription(
72+
{ name: parameterName, description }: { name: string; description: string; },
73+
) {
74+
return this.translateSpecific({
75+
key: `${this.activeNodeType}.parameters.${parameterName}.description`,
76+
fallback: description,
77+
});
78+
},
79+
80+
/**
81+
* Translate the name for an option in a `collection` or `fixed collection` parameter,
82+
* e.g. an option name in an "Additional Options" fixed collection.
83+
*/
84+
$translateCollectionOptionName(
85+
{ name: parameterName }: { name: string; },
86+
{ name: optionName, displayName }: { name: string; displayName: string; },
87+
) {
88+
return this.translateSpecific({
89+
key: `${this.activeNodeType}.parameters.${parameterName}.options.${optionName}.displayName`,
90+
fallback: displayName,
91+
});
92+
},
93+
94+
/**
95+
* Translate the label for a button that adds another field-input pair to a collection.
96+
*/
97+
$translateMultipleValueButtonText(
98+
{ name: parameterName, typeOptions: { multipleValueButtonText } }:
99+
{ name: string, typeOptions: { multipleValueButtonText: string } },
100+
) {
101+
return this.translateSpecific({
102+
key: `${this.activeNodeType}.parameters.${parameterName}.multipleValueButtonText`,
103+
fallback: multipleValueButtonText,
104+
});
105+
},
106+
107+
// -----------------------------------------
108+
// creds-specific methods
109+
// -----------------------------------------
110+
111+
/**
112+
* Translate a credentials property name, i.e. leftmost parameter in `CredentialsEdit.vue`.
113+
*/
114+
$translateCredentialsPropertyName(
115+
{ name: parameterName, displayName }: { name: string; displayName: string; },
116+
{ nodeType, credentialsName }: { nodeType: string, credentialsName: string; },
117+
) {
118+
if (['clientId', 'clientSecret'].includes(parameterName)) {
119+
return this.$t(`oauth2.${parameterName}`);
120+
}
121+
122+
return this.translateSpecific({
123+
key: `${nodeType}.credentials.${credentialsName}.${parameterName}.displayName`,
124+
fallback: displayName,
125+
});
126+
},
127+
128+
/**
129+
* Translate a credentials property description, i.e. label tooltip in `CredentialsEdit.vue`.
130+
*/
131+
$translateCredentialsPropertyDescription(
132+
{ name: parameterName, description }: { name: string; description: string; },
133+
{ nodeType, credentialsName }: { nodeType: string, credentialsName: string; },
134+
) {
135+
return this.translateSpecific({
136+
key: `${nodeType}.credentials.${credentialsName}.${parameterName}.description`,
137+
fallback: description,
138+
});
139+
},
140+
141+
// -----------------------------------------
142+
// node- and creds-specific methods
143+
// -----------------------------------------
144+
145+
/**
146+
* Translate the placeholder inside the input field for a string-type parameter.
147+
*/
148+
$translatePlaceholder(
149+
{ name: parameterName, placeholder }: { name: string; placeholder: string; },
150+
isCredential = false,
151+
{ nodeType, credentialsName } = { nodeType: '', credentialsName: '' },
152+
) {
153+
const key = isCredential
154+
? `${nodeType}.credentials.${credentialsName}.placeholder`
155+
: `${this.activeNodeType}.parameters.${parameterName}.placeholder`;
156+
157+
return this.translateSpecific({
158+
key,
159+
fallback: placeholder,
160+
});
161+
},
162+
163+
/**
164+
* Translate the name for an option in an `options` parameter,
165+
* e.g. an option name in a "Resource" or "Operation" dropdown menu.
166+
*/
167+
$translateOptionsOptionName(
168+
{ name: parameterName }: { name: string },
169+
{ value: optionName, name: displayName }: { value: string; name: string; },
170+
isCredential = false,
171+
{ nodeType, credentialsName } = { nodeType: '', credentialsName: '' },
172+
) {
173+
const key = isCredential
174+
? `${nodeType}.credentials.${credentialsName}.options.${optionName}.displayName`
175+
: `${this.activeNodeType}.parameters.${parameterName}.options.${optionName}.displayName`;
176+
177+
return this.translateSpecific({
178+
key,
179+
fallback: displayName,
180+
});
181+
},
182+
183+
/**
184+
* Translate the description for an option in an `options` parameter,
185+
* e.g. an option name in a "Resource" or "Operation" dropdown menu.
186+
*/
187+
$translateOptionsOptionDescription(
188+
{ name: parameterName }: { name: string },
189+
{ value: optionName, description }: { value: string; description: string; },
190+
isCredential = false,
191+
{ nodeType, credentialsName } = { nodeType: '', credentialsName: '' },
192+
) {
193+
const key = isCredential
194+
? `${nodeType}.credentials.${credentialsName}.options.${optionName}.description`
195+
: `${this.activeNodeType}.parameters.${parameterName}.options.${optionName}.description`;
196+
197+
return this.translateSpecific({
198+
key,
199+
fallback: description,
200+
});
201+
},
202+
},
203+
});

packages/editor-ui/src/i18n/index.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Vue from 'vue';
2+
import VueI18n from 'vue-i18n';
3+
import englishBaseText from './locales/en';
4+
import axios from 'axios';
5+
6+
Vue.use(VueI18n);
7+
8+
console.log('About to initialize i18n');
9+
10+
export const i18n = new VueI18n({
11+
locale: 'en',
12+
fallbackLocale: 'en',
13+
messages: englishBaseText,
14+
silentTranslationWarn: true,
15+
});
16+
17+
const loadedLanguages = ['en'];
18+
19+
function setLanguage(language: string): string {
20+
i18n.locale = language;
21+
axios.defaults.headers.common['Accept-Language'] = language;
22+
document!.querySelector('html')!.setAttribute('lang', language);
23+
return language;
24+
}
25+
26+
export async function loadLanguage(language?: string) {
27+
console.log(`loadLanguage called with ${language}`);
28+
29+
if (!language) return Promise.resolve();
30+
31+
if (i18n.locale === language) {
32+
return Promise.resolve(setLanguage(language));
33+
}
34+
35+
if (loadedLanguages.includes(language)) {
36+
return Promise.resolve(setLanguage(language));
37+
}
38+
39+
const { default: { [language]: messages }} = require(`./locales/${language}`);
40+
i18n.setLocaleMessage(language, messages);
41+
loadedLanguages.push(language);
42+
43+
return setLanguage(language);
44+
}
45+
46+
export function addNodeTranslations(translations: { [key: string]: string | object }) {
47+
const lang = Object.keys(translations)[0];
48+
const messages = translations[lang];
49+
const newNodesBase = {
50+
'n8n-nodes-base': Object.assign(
51+
i18n.messages[lang]['n8n-nodes-base'],
52+
messages,
53+
),
54+
};
55+
i18n.setLocaleMessage(lang, Object.assign(i18n.messages[lang], newNodesBase));
56+
}

0 commit comments

Comments
 (0)