i18n feedback refactorings (#2597)

*  Create endpoint for node credential translation

*  Add API helper method in FE

* 🔨 Add creds JSON files to tsconfig

*  Refactor credentials loading

*  Refactor calls in CredentialConfig

* ✏️ Add dummy translations

*  Split translations per node

* 🔥 Remove deprecated method

*  Refactor nesting in collections

* 🚚 Rename topParameter methods for accuracy

* ✏️ Fill out GitHub dummy cred

* 🚚 Clarify naming for collection utils

* ✏️ Fill out dummy translation

* 🔥 Remove surplus colons

* 🔥 Remove logging

*  Restore missing space

* 🔥 Remove lingering colon

*  Add path to InputLabel calls

* ✏️ Fill out dummy translations

* 🐛 Fix multipleValuesButtonText logic

*  Add sample properties to be deleted

*  Render deeply nested params

* 📦 Update package-lock.json

* 🔥 remove logging

* ✏️ Add dummy value to Slack translation

* ✏️ Add placeholder to dummy translation

*  Fix placeholder rendering for button text

* 👕 Fix lint

* 🔥 Remove outdated comment

* 🐛 Pass in missing arg for placeholder

* ✏️ Fill out Slack translation

*  Add explanatory comment

* ✏️ Fill out dummy translation

* ✏️ Update documentation

* 🔥 Remove broken link

* ✏️ Add pending functionality

* ✏️ Fix indentation

* 🐛 Fix method call in CredentialEdit

*  Implement eventTriggerDescription

* 🐛 Fix table-json-binary radio buttons

* ✏️ Clarify usage of eventTriggerDescription

* 🔥 Remove unneeded arg

* 🐛 Fix display in CodeEdit and TextEdit

* 🔥 Remove logging

* ✏️ Add translation for test cred options

* ✏️ Add test for separate file in same dir

* ✏️ Add test for versioned node

* ✏️ Add test for node in grouped dir

* ✏️ Add minor clarifications

* ✏️ Add nested collection test

* ✏️ Add pending functionality

*  Generalize collections handling

* 🚚 Rename helper to remove redundancy

* 🚚 Improve naming in helpers

* ✏️ Improve helpers documentation

* ✏️ Improve i18n methods documentation

* 🚚 Make endpoint naming consistent

* ✏️ Add final newlines

* ✏️ Clean up JSON examples

*  Reuse i18n method

*  Improve utils readability

*  Return early if cred translation exists

* 🔥 Remove dummy translations
This commit is contained in:
Iván Ovejero
2022-01-07 22:02:21 +01:00
committed by GitHub
parent 6a2db6d107
commit 5fec563c5c
30 changed files with 920 additions and 634 deletions

View File

@@ -4,14 +4,17 @@ import VueI18n from 'vue-i18n';
import { Store } from "vuex";
import Vue from 'vue';
import { INodeTranslationHeaders, IRootState } from '@/Interface';
import {
deriveMiddleKey,
isNestedInCollectionLike,
normalize,
insertOptionsAndValues,
} from "./utils";
const englishBaseText = require('./locales/en');
Vue.use(VueI18n);
const REUSABLE_DYNAMIC_TEXT_KEY = 'reusableDynamicText';
const CREDENTIALS_MODAL_KEY = 'credentialsModal';
const NODE_VIEW_KEY = 'nodeView';
export function I18nPlugin(vue: typeof _Vue, store: Store<IRootState>): void {
const i18n = new I18nClass(store);
@@ -43,10 +46,6 @@ export class I18nClass {
return this.i18n.te(key);
}
number(value: number, options: VueI18n.FormattedNumberPartType) {
return this.i18n.n(value, options);
}
shortNodeType(longNodeType: string) {
return longNodeType.replace('n8n-nodes-base.', '');
}
@@ -56,16 +55,17 @@ export class I18nClass {
// ----------------------------------
/**
* 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.
* Render a string of base text, i.e. a string with a fixed path to the localized value. 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.i18n.t(key, options && options.interpolate).toString();
}
/**
* 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, in the node view, or in the headers. Unlike in `baseText`, the fallback has to be set manually for dynamic text.
* Render a string of dynamic text, i.e. a string with a constructed path to the localized value.
*/
private dynamicRender(
{ key, fallback }: { key: string; fallback: string; },
@@ -74,30 +74,32 @@ export class I18nClass {
}
/**
* Render a string of dynamic header text, used in the nodes panel and in the node view.
* Render a string of header text (a node's name and description),
* used variously in the nodes panel, under the node icon, etc.
*/
headerText(arg: { key: string; fallback: string; }) {
return this.dynamicRender(arg);
}
/**
* Namespace for methods to render text in the credentials details modal.
*/
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 credentialType = this.$store.getters.activeCredentialType;
const credentialPrefix = `n8n-nodes-base.credentials.${credentialType}`;
const context = this;
return {
/**
* Display name for a top-level parameter in the credentials modal.
* Display name for a top-level param.
*/
topParameterDisplayName(
inputLabelDisplayName(
{ name: parameterName, displayName }: { name: string; displayName: string; },
) {
if (['clientId', 'clientSecret'].includes(parameterName)) {
return context.dynamicRender({
key: `${REUSABLE_DYNAMIC_TEXT_KEY}.oauth2.${parameterName}`,
key: `reusableDynamicText.oauth2.${parameterName}`,
fallback: displayName,
});
}
@@ -109,9 +111,9 @@ export class I18nClass {
},
/**
* Description for a top-level parameter in the credentials modal.
* Description (tooltip text) for an input label param.
*/
topParameterDescription(
inputLabelDescription(
{ name: parameterName, description }: { name: string; description: string; },
) {
return context.dynamicRender({
@@ -121,7 +123,7 @@ export class I18nClass {
},
/**
* Display name for an option inside an `options` or `multiOptions` parameter in the credentials modal.
* Display name for an option inside an `options` or `multiOptions` param.
*/
optionsOptionDisplayName(
{ name: parameterName }: { name: string; },
@@ -134,7 +136,7 @@ export class I18nClass {
},
/**
* Description for an option inside an `options` or `multiOptions` parameter in the credentials modal.
* Description for an option inside an `options` or `multiOptions` param.
*/
optionsOptionDescription(
{ name: parameterName }: { name: string; },
@@ -147,7 +149,7 @@ export class I18nClass {
},
/**
* Placeholder for a `string` or `collection` or `fixedCollection` parameter in the credentials modal.
* Placeholder for a `string` or `collection` or `fixedCollection` param.
* - For a `string` parameter, the placeholder is unselectable greyed-out sample text.
* - For a `collection` or `fixedCollection` parameter, the placeholder is the button text.
*/
@@ -162,99 +164,158 @@ export class I18nClass {
};
}
/**
* Namespace for methods to render text in the node details view,
* except for `eventTriggerDescription`.
*/
nodeText () {
const type = this.$store.getters.activeNode.type;
const nodePrefix = `${type}.${NODE_VIEW_KEY}`;
const activeNode = this.$store.getters.activeNode;
const nodeType = activeNode ? this.shortNodeType(activeNode.type) : ''; // unused in eventTriggerDescription
const initialKey = `n8n-nodes-base.nodes.${nodeType}.nodeView`;
const context = this;
return {
/**
* Display name for a top-level parameter in the node view.
* Display name for an input label, whether top-level or nested.
*/
topParameterDisplayName(
{ name: parameterName, displayName }: { name: string; displayName: string; },
inputLabelDisplayName(
parameter: { name: string; displayName: string; type: string },
path: string,
) {
const middleKey = deriveMiddleKey(path, parameter);
return context.dynamicRender({
key: `${nodePrefix}.${parameterName}.displayName`,
fallback: displayName,
key: `${initialKey}.${middleKey}.displayName`,
fallback: parameter.displayName,
});
},
/**
* Description for a top-level parameter in the node view in the node view.
* Description (tooltip text) for an input label, whether top-level or nested.
*/
topParameterDescription(
{ name: parameterName, description }: { name: string; description: string; },
inputLabelDescription(
parameter: { name: string; description: string; type: string },
path: string,
) {
const middleKey = deriveMiddleKey(path, parameter);
return context.dynamicRender({
key: `${nodePrefix}.${parameterName}.description`,
fallback: description,
key: `${initialKey}.${middleKey}.description`,
fallback: parameter.description,
});
},
/**
* Display name for an option inside a `collection` or `fixedCollection` parameter in the node view.
* Placeholder for an input label or `collection` or `fixedCollection` param,
* whether top-level or nested.
* - For an input label, the placeholder is unselectable greyed-out sample text.
* - For a `collection` or `fixedCollection`, the placeholder is the button text.
*/
collectionOptionDisplayName(
{ name: parameterName }: { name: string; },
{ name: optionName, displayName }: { name: string; displayName: string; },
placeholder(
parameter: { name: string; placeholder: string; type: string },
path: string,
) {
let middleKey = parameter.name;
if (isNestedInCollectionLike(path)) {
const pathSegments = normalize(path).split('.');
middleKey = insertOptionsAndValues(pathSegments).join('.');
}
return context.dynamicRender({
key: `${nodePrefix}.${parameterName}.options.${optionName}.displayName`,
fallback: displayName,
key: `${initialKey}.${middleKey}.placeholder`,
fallback: parameter.placeholder,
});
},
/**
* Display name for an option inside an `options` or `multiOptions` parameter in the node view.
* Display name for an option inside an `options` or `multiOptions` param,
* whether top-level or nested.
*/
optionsOptionDisplayName(
{ name: parameterName }: { name: string; },
parameter: { name: string; },
{ value: optionName, name: displayName }: { value: string; name: string; },
path: string,
) {
let middleKey = parameter.name;
if (isNestedInCollectionLike(path)) {
const pathSegments = normalize(path).split('.');
middleKey = insertOptionsAndValues(pathSegments).join('.');
}
return context.dynamicRender({
key: `${nodePrefix}.${parameterName}.options.${optionName}.displayName`,
key: `${initialKey}.${middleKey}.options.${optionName}.displayName`,
fallback: displayName,
});
},
/**
* Description for an option inside an `options` or `multiOptions` parameter in the node view.
* Description for an option inside an `options` or `multiOptions` param,
* whether top-level or nested.
*/
optionsOptionDescription(
{ name: parameterName }: { name: string; },
parameter: { name: string; },
{ value: optionName, description }: { value: string; description: string; },
path: string,
) {
let middleKey = parameter.name;
if (isNestedInCollectionLike(path)) {
const pathSegments = normalize(path).split('.');
middleKey = insertOptionsAndValues(pathSegments).join('.');
}
return context.dynamicRender({
key: `${nodePrefix}.${parameterName}.options.${optionName}.description`,
key: `${initialKey}.${middleKey}.options.${optionName}.description`,
fallback: description,
});
},
/**
* Text for a button to add another option inside a `collection` or `fixedCollection` parameter having`multipleValues: true` in the node view.
* Display name for an option in the dropdown menu of a `collection` or
* fixedCollection` param. No nesting support since `collection` cannot
* be nested in a `collection` or in a `fixedCollection`.
*/
collectionOptionDisplayName(
parameter: { name: string; },
{ name: optionName, displayName }: { name: string; displayName: string; },
path: string,
) {
let middleKey = parameter.name;
if (isNestedInCollectionLike(path)) {
const pathSegments = normalize(path).split('.');
middleKey = insertOptionsAndValues(pathSegments).join('.');
}
return context.dynamicRender({
key: `${initialKey}.${middleKey}.options.${optionName}.displayName`,
fallback: displayName,
});
},
/**
* Text for a button to add another option inside a `collection` or
* `fixedCollection` param having `multipleValues: true`.
*/
multipleValueButtonText(
{ name: parameterName, typeOptions: { multipleValueButtonText } }:
{ name: string; typeOptions: { multipleValueButtonText: string; } },
) {
return context.dynamicRender({
key: `${nodePrefix}.${parameterName}.multipleValueButtonText`,
key: `${initialKey}.${parameterName}.multipleValueButtonText`,
fallback: multipleValueButtonText,
});
},
/**
* Placeholder for a `string` or `collection` or `fixedCollection` parameter in the node view.
* - For a `string` parameter, the placeholder is unselectable greyed-out sample text.
* - For a `collection` or `fixedCollection` parameter, the placeholder is the button text.
*/
placeholder(
{ name: parameterName, placeholder }: { name: string; placeholder: string; },
eventTriggerDescription(
nodeType: string,
eventTriggerDescription: string,
) {
return context.dynamicRender({
key: `${nodePrefix}.${parameterName}.placeholder`,
fallback: placeholder,
key: `n8n-nodes-base.nodes.${nodeType}.nodeView.eventTriggerDescription`,
fallback: eventTriggerDescription,
});
},
};
@@ -302,14 +363,25 @@ export async function loadLanguage(language?: string) {
setLanguage(language);
}
/**
* Add a node translation to the i18n instance's `messages` object.
*/
export function addNodeTranslation(
nodeTranslation: { [key: string]: object },
nodeTranslation: { [nodeType: string]: object },
language: string,
) {
const oldNodesBase = i18nInstance.messages[language]['n8n-nodes-base'] || {};
const updatedNodes = {
// @ts-ignore
...oldNodesBase.nodes,
...nodeTranslation,
};
const newNodesBase = {
'n8n-nodes-base': Object.assign(
i18nInstance.messages[language]['n8n-nodes-base'] || {},
nodeTranslation,
oldNodesBase,
{ nodes: updatedNodes },
),
};
@@ -319,6 +391,37 @@ export function addNodeTranslation(
);
}
/**
* Add a credential translation to the i18n instance's `messages` object.
*/
export function addCredentialTranslation(
nodeCredentialTranslation: { [credentialType: string]: object },
language: string,
) {
const oldNodesBase = i18nInstance.messages[language]['n8n-nodes-base'] || {};
const updatedCredentials = {
// @ts-ignore
...oldNodesBase.credentials,
...nodeCredentialTranslation,
};
const newNodesBase = {
'n8n-nodes-base': Object.assign(
oldNodesBase,
{ credentials: updatedCredentials },
),
};
i18nInstance.setLocaleMessage(
language,
Object.assign(i18nInstance.messages[language], newNodesBase),
);
}
/**
* Add a node's header strings to the i18n instance's `messages` object.
*/
export function addHeaders(
headers: INodeTranslationHeaders,
language: string,
@@ -327,4 +430,4 @@ export function addHeaders(
language,
Object.assign(i18nInstance.messages[language], { headers }),
);
}
}