Add autocompletion for i18n keys in script sections of Vue files (#3133)

* 📘 Type `baseText()` to i18n keys

* 📘 Adjust `baseText()` signature

* 👕 Except JSON files from Vue ESLint

* 🐛 Fix errors surfaced by `baseText()` typing

*  Pluralize keys

* 📘 Add typing for category names

*  Mark internal keys

* ✏️ Update docs references

* 🎨 Prettify syntax

* 🐛 Fix leftover internal key references
This commit is contained in:
Iván Ovejero 2022-04-15 08:22:58 +02:00 committed by GitHub
parent 8532b0030d
commit 18dee373d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 95 additions and 55 deletions

View file

@ -90,8 +90,11 @@ export default mixins(showMessage).extend({
return this.userToDelete && !this.userToDelete.firstName;
},
title(): string {
const user = this.userToDelete && (this.userToDelete.fullName || this.userToDelete.email);
return this.$locale.baseText('settings.users.deleteUser', { interpolate: { user }});
const user = this.userToDelete && (this.userToDelete.fullName || this.userToDelete.email) || '';
return this.$locale.baseText(
'settings.users.deleteUser',
{ interpolate: { user }},
);
},
enabled(): boolean {
if (this.isPending) {
@ -138,7 +141,10 @@ export default mixins(showMessage).extend({
if (this.transferId) {
const getUserById = this.$store.getters['users/getUserById'];
const transferUser: IUser = getUserById(this.transferId);
message = this.$locale.baseText('settings.users.transferredToUser', { interpolate: { user: transferUser.fullName }});
message = this.$locale.baseText(
'settings.users.transferredToUser',
{ interpolate: { user: transferUser.fullName || '' }},
);
}
this.$showMessage({

View file

@ -106,7 +106,10 @@ export default mixins(showMessage).extend({
},
buttonLabel(): string {
if (this.emailsCount > 1) {
return this.$locale.baseText('settings.users.inviteXUser', { interpolate: { count: this.emailsCount }});
return this.$locale.baseText(
'settings.users.inviteXUser',
{ interpolate: { count: this.emailsCount.toString() }},
);
}
return this.$locale.baseText('settings.users.inviteUser');

View file

@ -125,13 +125,16 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
if (this.nodeType !== null && this.nodeType.hasOwnProperty('eventTriggerDescription')) {
const nodeName = this.$locale.shortNodeType(this.nodeType.name);
const { eventTriggerDescription } = this.nodeType;
return this.$locale.nodeText().eventTriggerDescription(nodeName, eventTriggerDescription);
return this.$locale.nodeText().eventTriggerDescription(
nodeName,
eventTriggerDescription || '',
);
} else {
return this.$locale.baseText(
'node.waitingForYouToCreateAnEventIn',
{
interpolate: {
nodeType: this.nodeType && getTriggerNodeServiceName(this.nodeType.displayName),
nodeType: this.nodeType ? getTriggerNodeServiceName(this.nodeType.displayName) : '',
},
},
);

View file

@ -15,6 +15,7 @@
<script lang="ts">
import Vue from 'vue';
import camelcase from 'lodash.camelcase';
import { CategoryName } from '@/plugins/i18n';
export default Vue.extend({
props: ['item'],
@ -24,8 +25,8 @@ export default Vue.extend({
},
},
methods: {
renderCategoryName(categoryName: string) {
const key = `nodeCreator.categoryNames.${categoryName}`;
renderCategoryName(categoryName: CategoryName) {
const key = `nodeCreator.categoryNames.${categoryName}` as const;
return this.$locale.exists(key) ? this.$locale.baseText(key) : categoryName;
},

View file

@ -330,7 +330,7 @@ export default mixins(
interpolate: {
defaultValue: this.defaultValues.saveDataErrorExecution === 'all'
? this.$locale.baseText('workflowSettings.saveDataErrorExecutionOptions.save')
: this.$locale.baseText('workflowSettings.saveDataErrorExecutionOptions.doNotsave'),
: this.$locale.baseText('workflowSettings.saveDataErrorExecutionOptions.doNotSave'),
},
},
),

View file

@ -34,14 +34,14 @@ When translating a string containing an interpolated variable, leave the variabl
### Reusable base text
As a convenience, the base text file may contain the special key `reusableBaseText`, which defines strings that can be shared among other strings with the syntax `@:reusableBaseText.key`, as follows:
As a convenience, the base text file may contain the special key `_reusableBaseText`, which defines strings that can be shared among other strings with the syntax `@:_reusableBaseText.key`, as follows:
```json
{
"reusableBaseText.save": "🇩🇪 Save",
"_reusableBaseText.save": "🇩🇪 Save",
"duplicateWorkflowDialog.enterWorkflowName": "🇩🇪 Enter workflow name",
"duplicateWorkflowDialog.save": "@:reusableBaseText.save",
"saveButton.save": "@:reusableBaseText.save",
"duplicateWorkflowDialog.save": "@:_reusableBaseText.save",
"saveButton.save": "@:_reusableBaseText.save",
"saveButton.saving": "🇩🇪 Saving",
"saveButton.saved": "🇩🇪 Saved",
}
@ -92,8 +92,8 @@ Currently only the keys `oauth.clientId` and `oauth.clientSecret` are supported
```json
{
"reusableDynamicText.oauth2.clientId": "🇩🇪 Client ID",
"reusableDynamicText.oauth2.clientSecret": "🇩🇪 Client Secret",
"_reusableDynamicText.oauth2.clientId": "🇩🇪 Client ID",
"_reusableDynamicText.oauth2.clientSecret": "🇩🇪 Client Secret",
}
```

View file

@ -14,7 +14,7 @@ import {
locale,
} from 'n8n-design-system';
const englishBaseText = require('./locales/en');
import englishBaseText from './locales/en.json';
Vue.use(VueI18n);
locale.use('en');
@ -62,8 +62,8 @@ export class I18nClass {
* 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?: { adjustToNumber: number; interpolate: { [key: string]: string } },
key: BaseTextKey,
options?: { adjustToNumber?: number; interpolate?: { [key: string]: string } },
): string {
if (options && options.adjustToNumber) {
return this.i18n.tc(key, options.adjustToNumber, options && options.interpolate).toString();
@ -107,7 +107,7 @@ export class I18nClass {
) {
if (['clientId', 'clientSecret'].includes(parameterName)) {
return context.dynamicRender({
key: `reusableDynamicText.oauth2.${parameterName}`,
key: `_reusableDynamicText.oauth2.${parameterName}`,
fallback: displayName,
});
}
@ -469,3 +469,23 @@ export function addHeaders(
Object.assign(i18nInstance.messages[language], { headers }),
);
}
// ----------------------------------
// typings
// ----------------------------------
declare module 'vue/types/vue' {
interface Vue {
$locale: I18nClass;
}
}
type GetBaseTextKey<T> = T extends `_${string}` ? never : T;
export type BaseTextKey = GetBaseTextKey<keyof typeof englishBaseText>;
type GetCategoryName<T> = T extends `nodeCreator.categoryNames.${infer C}`
? C
: never;
export type CategoryName = GetCategoryName<keyof typeof englishBaseText>;

View file

@ -1,4 +1,9 @@
{
"_reusableBaseText.cancel": "Cancel",
"_reusableBaseText.name": "Name",
"_reusableBaseText.save": "Save",
"_reusableDynamicText.oauth2.clientId": "Client ID",
"_reusableDynamicText.oauth2.clientSecret": "Client Secret",
"about.aboutN8n": "About n8n",
"about.close": "Close",
"about.license": "License",
@ -44,10 +49,8 @@
"auth.setup.ownerAccountBenefits": "Setting up an owner account allows you to invite others, and prevents people using n8n without an account",
"auth.setup.settingUpOwnerError": "Problem setting up owner",
"auth.setup.setupConfirmation.concatEntities": "{workflows} and {credentials}",
"auth.setup.setupConfirmation.credentialsCount": "{count} credentials",
"auth.setup.setupConfirmation.oneCredentialCount": "{count} credential",
"auth.setup.setupConfirmation.oneWorkflowCount": "{count} existing workflow",
"auth.setup.setupConfirmation.workflowsCount": "{count} existing workflows",
"auth.setup.setupConfirmation.credentials": "{count} credential | {count} credentials",
"auth.setup.setupConfirmation.existingWorkflows": "{count} existing workflow | {count} existing workflows",
"auth.setup.setupOwner": "Set up owner account",
"auth.setup.skipOwnerSetupQuestion": "Skip owner account setup?",
"auth.setup.skipSetup": "Skip setup",
@ -131,7 +134,7 @@
"credentialsList.deleteCredential": "Delete Credential",
"credentialsList.editCredential": "Edit Credential",
"credentialsList.errorLoadingCredentials": "Error loading credentials",
"credentialsList.name": "@:reusableBaseText.name",
"credentialsList.name": "@:_reusableBaseText.name",
"credentialsList.operations": "Operations",
"credentialsList.showError.deleteCredential.title": "Problem deleting credential",
"credentialsList.showMessage.title": "Credential deleted",
@ -144,11 +147,11 @@
"displayWithChange.cancelEdit": "Cancel Edit",
"displayWithChange.clickToChange": "Click to Change",
"displayWithChange.setValue": "Set Value",
"duplicateWorkflowDialog.cancel": "@:reusableBaseText.cancel",
"duplicateWorkflowDialog.cancel": "@:_reusableBaseText.cancel",
"duplicateWorkflowDialog.chooseOrCreateATag": "Choose or create a tag",
"duplicateWorkflowDialog.duplicateWorkflow": "Duplicate Workflow",
"duplicateWorkflowDialog.enterWorkflowName": "Enter workflow name",
"duplicateWorkflowDialog.save": "@:reusableBaseText.save",
"duplicateWorkflowDialog.save": "@:_reusableBaseText.save",
"duplicateWorkflowDialog.showMessage.message": "Please enter a name.",
"duplicateWorkflowDialog.showMessage.title": "Name missing",
"error": "Error",
@ -181,7 +184,7 @@
"executionsList.modes.retry": "retry",
"executionsList.modes.trigger": "trigger",
"executionsList.modes.webhook": "webhook",
"executionsList.name": "@:reusableBaseText.name",
"executionsList.name": "@:_reusableBaseText.name",
"executionsList.openPastExecution": "Open Past Execution",
"executionsList.retryExecution": "Retry execution",
"executionsList.retryOf": "Retry of",
@ -267,12 +270,12 @@
"mainSidebar.new": "New",
"mainSidebar.newTemplate": "New from template",
"mainSidebar.open": "Open",
"mainSidebar.prompt.cancel": "@:reusableBaseText.cancel",
"mainSidebar.prompt.cancel": "@:_reusableBaseText.cancel",
"mainSidebar.prompt.import": "Import",
"mainSidebar.prompt.importWorkflowFromUrl": "Import Workflow from URL",
"mainSidebar.prompt.invalidUrl": "Invalid URL",
"mainSidebar.prompt.workflowUrl": "Workflow URL",
"mainSidebar.save": "@:reusableBaseText.save",
"mainSidebar.save": "@:_reusableBaseText.save",
"mainSidebar.settings": "Settings",
"mainSidebar.showError.stopExecution.title": "Problem stopping execution",
"mainSidebar.showMessage.handleFileImport.message": "The file does not contain valid JSON data",
@ -427,7 +430,7 @@
"nodeView.loadingTemplate": "Loading template",
"nodeView.moreInfo": "More info",
"nodeView.noNodesGivenToAdd": "No nodes to add specified",
"nodeView.prompt.cancel": "@:reusableBaseText.cancel",
"nodeView.prompt.cancel": "@:_reusableBaseText.cancel",
"nodeView.prompt.invalidName": "Invalid Name",
"nodeView.prompt.newName": "New Name",
"nodeView.prompt.rename": "Rename",
@ -573,11 +576,6 @@
"pushConnection.showMessage.title": "Workflow executed successfully",
"pushConnectionTracker.cannotConnectToServer": "You have a connection issue or the server is down. <br />n8n should reconnect automatically once the issue is resolved.",
"pushConnectionTracker.connectionLost": "Connection lost",
"reusableBaseText.cancel": "Cancel",
"reusableBaseText.name": "Name",
"reusableBaseText.save": "Save",
"reusableDynamicText.oauth2.clientId": "Client ID",
"reusableDynamicText.oauth2.clientSecret": "Client Secret",
"runData.binary": "Binary",
"runData.copyItemPath": "Copy Item Path",
"runData.copyParameterPath": "Copy Parameter Path",
@ -600,7 +598,7 @@
"runData.showBinaryData": "View",
"runData.startTime": "Start Time",
"runData.table": "Table",
"saveButton.save": "@:reusableBaseText.save",
"saveButton.save": "@:_reusableBaseText.save",
"saveButton.saved": "Saved",
"saveButton.saving": "Saving",
"settings": "Settings",
@ -649,7 +647,7 @@
"settings.users.usersInvited": "Users invited",
"settings.users.usersInvitedError": "Could not invite users",
"settings.version": "Version",
"showMessage.cancel": "@:reusableBaseText.cancel",
"showMessage.cancel": "@:_reusableBaseText.cancel",
"showMessage.ok": "OK",
"showMessage.showDetails": "Show Details",
"startupError": "Error connecting to n8n",
@ -674,11 +672,11 @@
"tagsManager.showMessage.onUpdate.title": "Tag updated",
"tagsManager.tagNameCannotBeEmpty": "Tag name cannot be empty",
"tagsTable.areYouSureYouWantToDeleteThisTag": "Are you sure you want to delete this tag?",
"tagsTable.cancel": "@:reusableBaseText.cancel",
"tagsTable.cancel": "@:_reusableBaseText.cancel",
"tagsTable.createTag": "Create tag",
"tagsTable.deleteTag": "Delete tag",
"tagsTable.editTag": "Edit Tag",
"tagsTable.name": "@:reusableBaseText.name",
"tagsTable.name": "@:_reusableBaseText.name",
"tagsTable.noMatchingTagsExist": "No matching tags exist",
"tagsTable.saveChanges": "Save changes?",
"tagsTable.usage": "Usage",
@ -784,7 +782,7 @@
"workflowOpen.couldNotLoadActiveWorkflows": "Could not load active workflows",
"workflowOpen.created": "Created",
"workflowOpen.filterWorkflows": "Filter by tags",
"workflowOpen.name": "@:reusableBaseText.name",
"workflowOpen.name": "@:_reusableBaseText.name",
"workflowOpen.openWorkflow": "Open Workflow",
"workflowOpen.searchWorkflows": "Search workflows...",
"workflowOpen.showError.title": "Problem loading workflows",
@ -813,15 +811,15 @@
"workflowSettings.hours": "hours",
"workflowSettings.minutes": "minutes",
"workflowSettings.noWorkflow": "- No Workflow -",
"workflowSettings.save": "@:reusableBaseText.save",
"workflowSettings.save": "@:_reusableBaseText.save",
"workflowSettings.saveDataErrorExecution": "Save failed executions",
"workflowSettings.saveDataErrorExecutionOptions.defaultSave": "Default - {defaultValue}",
"workflowSettings.saveDataErrorExecutionOptions.doNotSave": "Do not save",
"workflowSettings.saveDataErrorExecutionOptions.save": "@:reusableBaseText.save",
"workflowSettings.saveDataErrorExecutionOptions.save": "@:_reusableBaseText.save",
"workflowSettings.saveDataSuccessExecution": "Save successful executions",
"workflowSettings.saveDataSuccessExecutionOptions.defaultSave": "Default - {defaultValue}",
"workflowSettings.saveDataSuccessExecutionOptions.doNotSave": "Do not save",
"workflowSettings.saveDataSuccessExecutionOptions.save": "@:reusableBaseText.save",
"workflowSettings.saveDataSuccessExecutionOptions.save": "@:_reusableBaseText.save",
"workflowSettings.saveExecutionProgress": "Save execution progress",
"workflowSettings.saveExecutionProgressOptions.defaultSave": "Default - {defaultValue}",
"workflowSettings.saveExecutionProgressOptions.no": "No",

View file

@ -1,7 +0,0 @@
import { i18nClass } from '.';
declare module 'vue/types/vue' {
interface Vue {
$locale: I18nClass;
}
}

View file

@ -83,7 +83,10 @@ export default mixins(showMessage).extend({
this.$showToast({
type: 'success',
title: this.$locale.baseText('settings.users.inviteResent'),
message: this.$locale.baseText('settings.users.emailSentTo', { interpolate: { email: user.email } }),
message: this.$locale.baseText(
'settings.users.emailSentTo',
{ interpolate: { email: user.email || '' } },
),
});
} catch (e) {
this.$showError(e, this.$locale.baseText('settings.users.userReinviteError'));

View file

@ -103,8 +103,19 @@ export default mixins(
return true;
}
const workflows = this.workflowsCount > 0 ? this.$locale.baseText(this.workflowsCount === 1 ? 'auth.setup.setupConfirmation.oneWorkflowCount' : 'auth.setup.setupConfirmation.workflowsCount', { interpolate: { count: this.workflowsCount } }) : '';
const credentials = this.credentialsCount > 0 ? this.$locale.baseText(this.credentialsCount === 1? 'auth.setup.setupConfirmation.oneCredentialCount' : 'auth.setup.setupConfirmation.credentialsCount', { interpolate: { count: this.credentialsCount } }) : '';
const workflows = this.workflowsCount > 0
? this.$locale.baseText(
'auth.setup.setupConfirmation.existingWorkflows',
{ adjustToNumber: this.workflowsCount },
)
: '';
const credentials = this.credentialsCount > 0
? this.$locale.baseText(
'auth.setup.setupConfirmation.credentials',
{ adjustToNumber: this.credentialsCount },
)
: '';
const entities = workflows && credentials ? this.$locale.baseText('auth.setup.setupConfirmation.concatEntities', {interpolate: { workflows, credentials }}) : (workflows || credentials);
return await this.confirmMessage(

View file

@ -13,6 +13,7 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"useUnknownInCatchVariables": false,
"resolveJsonModule": true,
"sourceMap": true,
"baseUrl": ".",
"types": [

View file

@ -2,7 +2,8 @@
"linterOptions": {
"exclude": [
"node_modules/**/*",
"../../node_modules/**/*"
"../../node_modules/**/*",
"**/*.json"
]
},
"defaultSeverity": "error",