From 52913bf2e6be86364f0285f456e91288e45f9ef9 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Fri, 11 Mar 2022 11:17:02 +0300 Subject: [PATCH 1/3] support i18n in nds --- .../src/components/N8nFormInput/FormInput.vue | 14 ++- .../src/components/N8nFormInput/validators.ts | 92 ++++++++++++------- .../src/components/N8nUserInfo/UserInfo.vue | 6 +- .../components/N8nUserSelect/UserSelect.vue | 21 +---- .../src/components/N8nUsersList/UsersList.vue | 10 +- .../design-system/src/components/index.js | 6 +- packages/design-system/src/locale/format.js | 55 +++++++++++ packages/design-system/src/locale/index.js | 39 ++++++++ packages/design-system/src/locale/lang/en.js | 19 ++++ packages/design-system/src/mixins/locale.js | 9 ++ packages/editor-ui/src/Interface.ts | 4 +- .../src/components/ChangePasswordModal.vue | 12 ++- .../src/components/InviteUsersModal.vue | 19 +++- packages/editor-ui/src/plugins/i18n/index.ts | 4 + .../src/views/ChangePasswordView.vue | 12 ++- 15 files changed, 253 insertions(+), 69 deletions(-) create mode 100644 packages/design-system/src/locale/format.js create mode 100644 packages/design-system/src/locale/index.js create mode 100644 packages/design-system/src/locale/lang/en.js create mode 100644 packages/design-system/src/mixins/locale.js diff --git a/packages/design-system/src/components/N8nFormInput/FormInput.vue b/packages/design-system/src/components/N8nFormInput/FormInput.vue index f545178ff226e..6594faa712161 100644 --- a/packages/design-system/src/components/N8nFormInput/FormInput.vue +++ b/packages/design-system/src/components/N8nFormInput/FormInput.vue @@ -58,7 +58,10 @@ import N8nInputLabel from '../N8nInputLabel'; import { getValidationError, VALIDATORS } from './validators'; import { Rule, RuleGroup, IValidator } from "../../../../editor-ui/src/Interface"; -export default Vue.extend({ +import Locale from '../../mixins/locale'; +import mixins from 'vue-typed-mixins'; + +export default mixins(Locale).extend({ name: 'n8n-form-input', components: { N8nInput, @@ -133,7 +136,12 @@ export default Vue.extend({ }, computed: { validationError(): string | null { - return this.getValidationError(); + const error = this.getValidationError(); + if (error) { + return this.t(error.messageKey, error.options); + } + + return null; }, hasDefaultSlot(): boolean { return !!this.$slots.default; @@ -146,7 +154,7 @@ export default Vue.extend({ }, }, methods: { - getValidationError(): string | null { + getValidationError(): ReturnType { const rules = (this.validationRules || []) as (Rule | RuleGroup)[]; const validators = { ...VALIDATORS, diff --git a/packages/design-system/src/components/N8nFormInput/validators.ts b/packages/design-system/src/components/N8nFormInput/validators.ts index c4c8a7dc34231..963e533e5594d 100644 --- a/packages/design-system/src/components/N8nFormInput/validators.ts +++ b/packages/design-system/src/components/N8nFormInput/validators.ts @@ -8,54 +8,85 @@ export const VALIDATORS: { [key: string]: IValidator | RuleGroup } = { REQUIRED: { validate: (value: string | number | boolean | null | undefined) => { if (typeof value === 'string' && !!value.trim()) { - return; + return false; } if (typeof value === 'number' || typeof value === 'boolean') { - return; + return false; } - throw new Error('This field is required'); + + return { + messageKey: 'formInput.validator.fieldRequired', + }; }, }, MIN_LENGTH: { - validate: (value: string, config: { minimum: number }) => { - if (value.length < config.minimum) { - throw new Error(`Must be at least ${config.minimum} characters`); + validate: (value: string | number | boolean | null | undefined, config: { minimum: number }) => { + if (typeof value === 'string' && value.length < config.minimum) { + return { + messageKey: 'formInput.validator.minCharactersRequired', + options: config, + }; } + + return false; }, }, MAX_LENGTH: { - validate: (value: string, config: { maximum: number }) => { - if (value.length > config.maximum) { - throw new Error(`Must be at most ${config.maximum} characters`); + validate: (value: string | number | boolean | null | undefined, config: { maximum: number }) => { + if (typeof value === 'string' && value.length > config.maximum) { + return { + messageKey: 'formInput.validator.maxCharactersRequired', + options: config, + }; } + + return false; }, }, CONTAINS_NUMBER: { - validate: (value: string, config: { minimum: number }) => { - const numberCount = (value.match(/\d/g) || []).length; + validate: (value: string | number | boolean | null | undefined, config: { minimum: number }) => { + if (typeof value !== 'string') { + return false; + } + const numberCount = (value.match(/\d/g) || []).length; if (numberCount < config.minimum) { - throw new Error(`Must have at least ${config.minimum} number${config.minimum > 1 ? 's' : ''}`); + return { + messageKey: 'formInput.validator.numbersRequired', + options: config, + }; } + + return false; }, }, VALID_EMAIL: { - validate: (value: string) => { + validate: (value: string | number | boolean | null | undefined) => { if (!emailRegex.test(String(value).trim().toLowerCase())) { - throw new Error('Must be a valid email'); + return { + messageKey: 'formInput.validator.validEmailRequired', + }; } + + return false; }, }, CONTAINS_UPPERCASE: { - validate: (value: string, config: { minimum: number }) => { - const uppercaseCount = (value.match(/[A-Z]/g) || []).length; + validate: (value: string | number | boolean | null | undefined, config: { minimum: number }) => { + if (typeof value !== 'string') { + return false; + } + const uppercaseCount = (value.match(/[A-Z]/g) || []).length; if (uppercaseCount < config.minimum) { - throw new Error(`Must have at least ${config.minimum} uppercase character${ - config.minimum > 1 ? 's' : '' - }`); + return { + messageKey: 'formInput.validator.uppercaseCharsRequired', + options: config, + }; } + + return false; }, }, DEFAULT_PASSWORD_RULES: { @@ -66,7 +97,9 @@ export const VALIDATORS: { [key: string]: IValidator | RuleGroup } = { { name: 'CONTAINS_NUMBER', config: { minimum: 1 } }, { name: 'CONTAINS_UPPERCASE', config: { minimum: 1 } }, ], - defaultError: '8+ characters, at least 1 number and 1 capital letter', + defaultError: { + messageKey: 'formInput.validator.defaultPasswordRequirements', + }, }, { name: 'MAX_LENGTH', config: {maximum: 64} }, ], @@ -78,7 +111,7 @@ export const getValidationError = ( validators: { [key: string]: IValidator | RuleGroup }, validator: IValidator | RuleGroup, config?: any, // tslint:disable-line:no-any -): string | null => { +): ReturnType => { if (validator.hasOwnProperty('rules')) { const rules = (validator as RuleGroup).rules; for (let i = 0; i < rules.length; i++) { @@ -107,22 +140,19 @@ export const getValidationError = ( validators[rule.name] as IValidator, rule.config, ); - if (error) { - return (validator as RuleGroup).defaultError || error; + if (error && (validator as RuleGroup).defaultError !== undefined) { + // @ts-ignore + return validator.defaultError; + } else if (error) { + return error; } } } } else if ( validator.hasOwnProperty('validate') ) { - try { - (validator as IValidator).validate(value, config); - } catch (e: unknown) { - if (e instanceof Error) { - return e.message; - } - } + return (validator as IValidator).validate(value, config); } - return null; + return false; }; diff --git a/packages/design-system/src/components/N8nUserInfo/UserInfo.vue b/packages/design-system/src/components/N8nUserInfo/UserInfo.vue index 12bcc800a9edd..e6acb476ea312 100644 --- a/packages/design-system/src/components/N8nUserInfo/UserInfo.vue +++ b/packages/design-system/src/components/N8nUserInfo/UserInfo.vue @@ -10,7 +10,7 @@
- {{firstName}} {{lastName}} {{isCurrentUser ? '(you)' : ''}} + {{firstName}} {{lastName}} {{isCurrentUser ? this.t('nds.userInfo.you') : ''}}
{{email}} @@ -25,8 +25,10 @@ import Vue from 'vue'; import N8nText from '../N8nText'; import N8nAvatar from '../N8nAvatar'; import N8nBadge from '../N8nBadge'; +import Locale from '../../mixins/locale'; +import mixins from 'vue-typed-mixins'; -export default Vue.extend({ +export default mixins(Locale).extend({ name: 'n8n-users-info', components: { N8nAvatar, diff --git a/packages/design-system/src/components/N8nUserSelect/UserSelect.vue b/packages/design-system/src/components/N8nUserSelect/UserSelect.vue index a9845da757e32..620c34b24112f 100644 --- a/packages/design-system/src/components/N8nUserSelect/UserSelect.vue +++ b/packages/design-system/src/components/N8nUserSelect/UserSelect.vue @@ -3,12 +3,11 @@ :value="value" :filterable="true" :filterMethod="setFilter" - :placeholder="placeholder" + :placeholder="t('nds.userSelect.selectUser')" :default-first-option="true" :popper-append-to-body="true" :popper-class="$style.limitPopperWidth" - :noMatchText="noMatchText" - :noDataText="noDataText" + :noDataText="t('nds.userSelect.noMatchingUsers')" @change="onChange" @blur="onBlur" @focus="onFocus" @@ -31,8 +30,10 @@ import N8nUserInfo from '../N8nUserInfo'; import { IUser } from '../../Interface'; import ElSelect from 'element-ui/lib/select'; import ElOption from 'element-ui/lib/option'; +import Locale from '../../mixins/locale'; +import mixins from 'vue-typed-mixins'; -export default Vue.extend({ +export default mixins(Locale).extend({ name: 'n8n-user-select', components: { N8nUserInfo, @@ -57,18 +58,6 @@ export default Vue.extend({ }, validator: (ids: string[]) => !ids.find((id) => typeof id !== 'string'), }, - placeholder: { - type: String, - default: 'Select user', - }, - noMatchText: { - type: String, - default: 'No matches found', - }, - noDataText: { - type: String, - default: 'No users', - }, currentUserId: { type: String, }, diff --git a/packages/design-system/src/components/N8nUsersList/UsersList.vue b/packages/design-system/src/components/N8nUsersList/UsersList.vue index 3d9066329017f..f77ea998834cf 100644 --- a/packages/design-system/src/components/N8nUsersList/UsersList.vue +++ b/packages/design-system/src/components/N8nUsersList/UsersList.vue @@ -7,7 +7,7 @@ >
- Owner + {{ t('nds.auth.roles.owner') }} { + let result; + + if (string[index - 1] === '{' && + string[index + match.length] === '}') { + return i; + } else { + result = hasOwn(args, i) ? args[i] : null; + if (result === null || result === undefined) { + return ''; + } + + return result; + } + }); + } + + return template; +} diff --git a/packages/design-system/src/locale/index.js b/packages/design-system/src/locale/index.js new file mode 100644 index 0000000000000..f685b6eccc4c7 --- /dev/null +++ b/packages/design-system/src/locale/index.js @@ -0,0 +1,39 @@ +import defaultLang from '../locale/lang/en'; +import Vue from 'vue'; +import Format from './format'; + +import ElementLocale from 'element-ui/lib/locale'; +import ElementLang from 'element-ui/lib/locale/lang/en'; +ElementLocale.use(ElementLang); + +const format = Format(Vue); +let lang = defaultLang; + +export const t = function(path, options) { + // only support flat keys + if (lang[path] !== undefined) { + return format(lang[path], options); + } + + return ''; +}; + +function override(current, overides = {}) { + return { + ...current, + ...overides, + }; +} + +export const use = function(l, overrides) { + try { + const ndsLang = require(`./lang/${l}`); + lang = override(ndsLang.default, overrides); + + const elLang = require(`element-ui/lib/locale/lang/${l}`);; + ElementLocale.use(elLang); + } catch (e) { + } +}; + +export default { use, t }; diff --git a/packages/design-system/src/locale/lang/en.js b/packages/design-system/src/locale/lang/en.js new file mode 100644 index 0000000000000..7fc1a3301e7aa --- /dev/null +++ b/packages/design-system/src/locale/lang/en.js @@ -0,0 +1,19 @@ +export default { + 'nds.auth.roles.owner': 'Owner', + 'nds.userInfo.you': '(you)', + 'nds.userSelect.selectUser': 'Select User', + 'nds.userSelect.noMatchingUsers': 'No matching users', + 'nds.usersList.deleteUser': 'Delete User', + 'nds.usersList.reinviteUser': 'Resend invite', + 'formInput.validator.fieldRequired': 'This field is required', + 'formInput.validator.minCharactersRequired': 'Must be at least {minimum} characters', + 'formInput.validator.maxCharactersRequired': 'Must be at most {maximum} characters', + 'formInput.validator.oneNumbersRequired': (config) => { + return `Must have at least ${config.minimum} number${config.minimum > 1 ? 's' : ''}`; + }, + 'formInput.validator.validEmailRequired': 'Must be a valid email', + 'formInput.validator.uppercaseCharsRequired': (config) => (`Must have at least ${config.minimum} uppercase character${ + config.minimum > 1 ? 's' : '' + }`), + "formInput.validator.defaultPasswordRequirements": "8+ characters, at least 1 number and 1 capital letter", +}; diff --git a/packages/design-system/src/mixins/locale.js b/packages/design-system/src/mixins/locale.js new file mode 100644 index 0000000000000..888d8a8307033 --- /dev/null +++ b/packages/design-system/src/mixins/locale.js @@ -0,0 +1,9 @@ +import { t } from '../locale'; + +export default { + methods: { + t(...args) { + return t.apply(this, args); + }, + }, +}; diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index c8551fe8d4428..a9b8ea348aa11 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -918,11 +918,11 @@ export type Rule = { name: string; config?: any}; // tslint:disable-line:no-any export type RuleGroup = { rules: Array; - defaultError?: string; + defaultError?: {messageKey: string, options?: any}; // tslint-disable-line:no-any }; export type IValidator = { - validate: Function; + validate: (value: string | number | boolean | null | undefined, config: any) => false | {messageKey: string, options?: any}; // tslint:disable-line:no-any }; export type IFormInput = { diff --git a/packages/editor-ui/src/components/ChangePasswordModal.vue b/packages/editor-ui/src/components/ChangePasswordModal.vue index fe07c6d5f7e85..893218a99865e 100644 --- a/packages/editor-ui/src/components/ChangePasswordModal.vue +++ b/packages/editor-ui/src/components/ChangePasswordModal.vue @@ -93,10 +93,18 @@ export default mixins(showMessage).extend({ ]; }, methods: { - passwordsMatch(value: string) { + passwordsMatch(value: string | number | boolean | null | undefined) { + if (typeof value !== 'string') { + return false; + } + if (value !== this.password) { - throw new Error(this.$locale.baseText('auth.changePassword.passwordsMustMatchError')); + return { + messageKey: 'auth.changePassword.passwordsMustMatchError', + }; } + + return false; }, onInput(e: {name: string, value: string}) { if (e.name === 'password') { diff --git a/packages/editor-ui/src/components/InviteUsersModal.vue b/packages/editor-ui/src/components/InviteUsersModal.vue index 87ad1e262a445..ac0e522e5be02 100644 --- a/packages/editor-ui/src/components/InviteUsersModal.vue +++ b/packages/editor-ui/src/components/InviteUsersModal.vue @@ -116,14 +116,25 @@ export default mixins(showMessage).extend({ }, }, methods: { - validateEmails(value: string) { - value.split(',').forEach((email: string) => { + validateEmails(value: string | number | boolean | null | undefined) { + if (typeof value !== 'string') { + return false; + } + + const emails = value.split(','); + for (let i = 0; i < emails.length; i++) { + const email = emails[i]; const parsed = getEmail(email); if (!!parsed.trim() && !VALID_EMAIL_REGEX.test(String(parsed).trim().toLowerCase())) { - throw new Error(this.$locale.baseText('settings.users.invalidEmailError', { interpolate: { email: parsed }})); + return { + messageKey: 'settings.users.invalidEmailError', + options: { interpolate: { email: parsed }}, + }; } - }); + } + + return false; }, onInput(e: {name: string, value: string}) { if (e.name === 'emails') { diff --git a/packages/editor-ui/src/plugins/i18n/index.ts b/packages/editor-ui/src/plugins/i18n/index.ts index bb1d50d359253..e249d0bc1345d 100644 --- a/packages/editor-ui/src/plugins/i18n/index.ts +++ b/packages/editor-ui/src/plugins/i18n/index.ts @@ -10,10 +10,14 @@ import { normalize, insertOptionsAndValues, } from "./utils"; +import { + locale, +} from 'n8n-design-system'; const englishBaseText = require('./locales/en'); Vue.use(VueI18n); +locale.use('en', englishBaseText); export function I18nPlugin(vue: typeof _Vue, store: Store): void { const i18n = new I18nClass(store); diff --git a/packages/editor-ui/src/views/ChangePasswordView.vue b/packages/editor-ui/src/views/ChangePasswordView.vue index 7b9b71d6ff084..3f1ae8d972fc3 100644 --- a/packages/editor-ui/src/views/ChangePasswordView.vue +++ b/packages/editor-ui/src/views/ChangePasswordView.vue @@ -84,10 +84,18 @@ export default mixins( } }, methods: { - passwordsMatch(value: string) { + passwordsMatch(value: string | number | boolean | null | undefined) { + if (typeof value !== 'string') { + return false; + } + if (value !== this.password) { - throw new Error(this.$locale.baseText('auth.changePassword.passwordsMustMatchError')); + return { + messageKey: 'auth.changePassword.passwordsMustMatchError', + }; } + + return false; }, onInput(e: {name: string, value: string}) { if (e.name === 'password') { From b0f989c9897563b4dd6df3b0bd0f012045d9dbeb Mon Sep 17 00:00:00 2001 From: Mutasem Date: Fri, 11 Mar 2022 11:48:04 +0300 Subject: [PATCH 2/3] fix how external keys are handled --- packages/design-system/src/locale/index.js | 23 ++++++++++++-------- packages/editor-ui/src/Interface.ts | 2 +- packages/editor-ui/src/plugins/i18n/index.ts | 7 +++++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/design-system/src/locale/index.js b/packages/design-system/src/locale/index.js index f685b6eccc4c7..d251a22187ac9 100644 --- a/packages/design-system/src/locale/index.js +++ b/packages/design-system/src/locale/index.js @@ -9,7 +9,14 @@ ElementLocale.use(ElementLang); const format = Format(Vue); let lang = defaultLang; +let i18nHandler; + export const t = function(path, options) { + if (typeof i18nHandler === 'function') { + const value = i18nHandler.apply(this, arguments); + if (value !== null && value !== undefined && value !== path) return value; + } + // only support flat keys if (lang[path] !== undefined) { return format(lang[path], options); @@ -18,17 +25,11 @@ export const t = function(path, options) { return ''; }; -function override(current, overides = {}) { - return { - ...current, - ...overides, - }; -} +export const use = function(l) { -export const use = function(l, overrides) { try { const ndsLang = require(`./lang/${l}`); - lang = override(ndsLang.default, overrides); + lang = ndsLang.default; const elLang = require(`element-ui/lib/locale/lang/${l}`);; ElementLocale.use(elLang); @@ -36,4 +37,8 @@ export const use = function(l, overrides) { } }; -export default { use, t }; +export const i18n = function(fn) { + i18nHandler = fn || i18nHandler; +}; + +export default { use, t, i18n }; diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index a9b8ea348aa11..2a7500300cb89 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -918,7 +918,7 @@ export type Rule = { name: string; config?: any}; // tslint:disable-line:no-any export type RuleGroup = { rules: Array; - defaultError?: {messageKey: string, options?: any}; // tslint-disable-line:no-any + defaultError?: {messageKey: string, options?: any}; // tslint:disable-line:no-any }; export type IValidator = { diff --git a/packages/editor-ui/src/plugins/i18n/index.ts b/packages/editor-ui/src/plugins/i18n/index.ts index e249d0bc1345d..fe1d681bfa246 100644 --- a/packages/editor-ui/src/plugins/i18n/index.ts +++ b/packages/editor-ui/src/plugins/i18n/index.ts @@ -17,7 +17,7 @@ import { const englishBaseText = require('./locales/en'); Vue.use(VueI18n); -locale.use('en', englishBaseText); +locale.use('en'); export function I18nPlugin(vue: typeof _Vue, store: Store): void { const i18n = new I18nClass(store); @@ -358,6 +358,8 @@ const i18nInstance = new VueI18n({ silentTranslationWarn: true, }); +locale.i18n((key: string, options?: {interpolate: object}) => i18nInstance.t(key, options && options.interpolate)); + const loadedLanguages = ['en']; function setLanguage(language: string) { @@ -365,6 +367,9 @@ function setLanguage(language: string) { axios.defaults.headers.common['Accept-Language'] = language; document!.querySelector('html')!.setAttribute('lang', language); + // update n8n design system and element ui + locale.use(language); + return language; } From d628f8189a76627efa17d2b3bb75714316272619 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Fri, 11 Mar 2022 13:03:16 +0300 Subject: [PATCH 3/3] add source --- packages/design-system/src/locale/format.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/design-system/src/locale/format.js b/packages/design-system/src/locale/format.js index d69e16cab897b..cd093f2b712c4 100644 --- a/packages/design-system/src/locale/format.js +++ b/packages/design-system/src/locale/format.js @@ -9,6 +9,7 @@ const RE_NARGS = /(%|)\{([0-9a-zA-Z_]+)\}/g; /** * String format template * - Inspired: + * https://github.com/ElemeFE/element/blob/dev/src/locale/format.js * https://github.com/Matt-Esch/string-template/index.js */ export default function(Vue) {