diff --git a/packages/cli/src/auth/auth.service.ts b/packages/cli/src/auth/auth.service.ts index 0181b62d1e..f0a3b99283 100644 --- a/packages/cli/src/auth/auth.service.ts +++ b/packages/cli/src/auth/auth.service.ts @@ -5,6 +5,7 @@ import { Service } from '@n8n/di'; import { createHash } from 'crypto'; import type { NextFunction, Response } from 'express'; import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken'; +import type { StringValue as TimeUnitValue } from 'ms'; import { Logger } from 'n8n-core'; import config from '@/config'; @@ -180,7 +181,7 @@ export class AuthService { return user; } - generatePasswordResetToken(user: User, expiresIn = '20m') { + generatePasswordResetToken(user: User, expiresIn: TimeUnitValue = '20m') { const payload: PasswordResetToken = { sub: user.id, hash: this.createJWTHash(user) }; return this.jwtService.sign(payload, { expiresIn }); } diff --git a/packages/core/src/binary-data/binary-data.service.ts b/packages/core/src/binary-data/binary-data.service.ts index b8b21d6f83..6085e27f57 100644 --- a/packages/core/src/binary-data/binary-data.service.ts +++ b/packages/core/src/binary-data/binary-data.service.ts @@ -1,5 +1,6 @@ import { Container, Service } from '@n8n/di'; import jwt from 'jsonwebtoken'; +import type { StringValue as TimeUnitValue } from 'ms'; import { BINARY_ENCODING, UnexpectedError } from 'n8n-workflow'; import type { INodeExecutionData, IBinaryData } from 'n8n-workflow'; import { readFile, stat } from 'node:fs/promises'; @@ -45,7 +46,7 @@ export class BinaryDataService { } } - createSignedToken(binaryData: IBinaryData, expiresIn = '1 day') { + createSignedToken(binaryData: IBinaryData, expiresIn: TimeUnitValue = '1 day') { if (!binaryData.id) { throw new UnexpectedError('URL signing is not available in memory mode'); } diff --git a/packages/core/src/execution-engine/node-execution-context/utils/binary-helper-functions.ts b/packages/core/src/execution-engine/node-execution-context/utils/binary-helper-functions.ts index d111a93be5..9a34622127 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/binary-helper-functions.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/binary-helper-functions.ts @@ -4,6 +4,7 @@ import FileType from 'file-type'; import { IncomingMessage } from 'http'; import iconv from 'iconv-lite'; import { extension, lookup } from 'mime-types'; +import type { StringValue as TimeUnitValue } from 'ms'; import type { BinaryHelperFunctions, IBinaryData, @@ -279,7 +280,7 @@ export const getBinaryHelperFunctions = ( getBinaryMetadata, binaryToBuffer, binaryToString, - createBinarySignedUrl(binaryData: IBinaryData, expiresIn?: string) { + createBinarySignedUrl(binaryData: IBinaryData, expiresIn?: TimeUnitValue) { const token = Container.get(BinaryDataService).createSignedToken(binaryData, expiresIn); return `${restApiUrl}/binary-data/signed?token=${token}`; }, diff --git a/packages/frontend/@n8n/composables/tsup.config.ts b/packages/frontend/@n8n/composables/tsup.config.ts index 0d555b1ab0..bff21e2550 100644 --- a/packages/frontend/@n8n/composables/tsup.config.ts +++ b/packages/frontend/@n8n/composables/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ - entry: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/*.d.ts', '!src/__tests__**/*'], + entry: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/*.d.ts', '!src/__tests__/**/*'], format: ['cjs', 'esm'], clean: true, dts: true, diff --git a/packages/frontend/@n8n/i18n/.eslintrc.cjs b/packages/frontend/@n8n/i18n/.eslintrc.cjs new file mode 100644 index 0000000000..3f9a316c08 --- /dev/null +++ b/packages/frontend/@n8n/i18n/.eslintrc.cjs @@ -0,0 +1,10 @@ +const sharedOptions = require('@n8n/eslint-config/shared'); + +/** + * @type {import('@types/eslint').ESLint.ConfigData} + */ +module.exports = { + extends: ['@n8n/eslint-config/frontend'], + + ...sharedOptions(__dirname, 'frontend'), +}; diff --git a/packages/frontend/@n8n/i18n/.gitignore b/packages/frontend/@n8n/i18n/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/packages/frontend/@n8n/i18n/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/frontend/@n8n/i18n/README.md b/packages/frontend/@n8n/i18n/README.md new file mode 100644 index 0000000000..7b0020ca88 --- /dev/null +++ b/packages/frontend/@n8n/i18n/README.md @@ -0,0 +1,27 @@ +# @n8n/i18n + +A package for managing internationalization (i18n) in n8n's Frontend codebase. It provides a structured way to handle translations and localization, ensuring that the application can be easily adapted to different languages and regions. + +## Table of Contents + +- [Features](#features) +- [Contributing](#contributing) +- [License](#license) + +## Features + +- **Translation Management**: Simplifies the process of managing translations for different languages. +- **Localization Support**: Provides tools to adapt the application for different regions and cultures. +- **Easy Integration**: Seamlessly integrates with n8n's Frontend codebase, making it easy to implement and use. +- **Reusable Base Text**: Allows for the definition of reusable base text strings, reducing redundancy in translations. +- **Pluralization and Interpolation**: Supports pluralization and interpolation in base text strings, making it flexible for various use cases. +- **Versioned Nodes Support**: Facilitates the management of translations for nodes in versioned directories, ensuring consistency across different versions. +- **Documentation**: Comprehensive documentation to help developers understand and utilize the package effectively. + +## Contributing + +For more details, please read our [CONTRIBUTING.md](CONTRIBUTING.md). + +## License + +For more details, please read our [LICENSE.md](LICENSE.md). diff --git a/packages/frontend/@n8n/i18n/biome.jsonc b/packages/frontend/@n8n/i18n/biome.jsonc new file mode 100644 index 0000000000..f882da95a5 --- /dev/null +++ b/packages/frontend/@n8n/i18n/biome.jsonc @@ -0,0 +1,4 @@ +{ + "$schema": "../../../../node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["../../../../biome.jsonc"] +} diff --git a/packages/frontend/editor-ui/src/plugins/i18n/docs/ADDENDUM.md b/packages/frontend/@n8n/i18n/docs/ADDENDUM.md similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/docs/ADDENDUM.md rename to packages/frontend/@n8n/i18n/docs/ADDENDUM.md diff --git a/packages/frontend/editor-ui/src/plugins/i18n/docs/README.md b/packages/frontend/@n8n/i18n/docs/README.md similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/docs/README.md rename to packages/frontend/@n8n/i18n/docs/README.md diff --git a/packages/frontend/editor-ui/src/plugins/i18n/docs/img/cred.png b/packages/frontend/@n8n/i18n/docs/img/cred.png similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/docs/img/cred.png rename to packages/frontend/@n8n/i18n/docs/img/cred.png diff --git a/packages/frontend/editor-ui/src/plugins/i18n/docs/img/header1.png b/packages/frontend/@n8n/i18n/docs/img/header1.png similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/docs/img/header1.png rename to packages/frontend/@n8n/i18n/docs/img/header1.png diff --git a/packages/frontend/editor-ui/src/plugins/i18n/docs/img/header2.png b/packages/frontend/@n8n/i18n/docs/img/header2.png similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/docs/img/header2.png rename to packages/frontend/@n8n/i18n/docs/img/header2.png diff --git a/packages/frontend/editor-ui/src/plugins/i18n/docs/img/header3.png b/packages/frontend/@n8n/i18n/docs/img/header3.png similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/docs/img/header3.png rename to packages/frontend/@n8n/i18n/docs/img/header3.png diff --git a/packages/frontend/editor-ui/src/plugins/i18n/docs/img/header4.png b/packages/frontend/@n8n/i18n/docs/img/header4.png similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/docs/img/header4.png rename to packages/frontend/@n8n/i18n/docs/img/header4.png diff --git a/packages/frontend/editor-ui/src/plugins/i18n/docs/img/header5.png b/packages/frontend/@n8n/i18n/docs/img/header5.png similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/docs/img/header5.png rename to packages/frontend/@n8n/i18n/docs/img/header5.png diff --git a/packages/frontend/editor-ui/src/plugins/i18n/docs/img/node1.png b/packages/frontend/@n8n/i18n/docs/img/node1.png similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/docs/img/node1.png rename to packages/frontend/@n8n/i18n/docs/img/node1.png diff --git a/packages/frontend/editor-ui/src/plugins/i18n/docs/img/node2.png b/packages/frontend/@n8n/i18n/docs/img/node2.png similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/docs/img/node2.png rename to packages/frontend/@n8n/i18n/docs/img/node2.png diff --git a/packages/frontend/editor-ui/src/plugins/i18n/docs/img/node4.png b/packages/frontend/@n8n/i18n/docs/img/node4.png similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/docs/img/node4.png rename to packages/frontend/@n8n/i18n/docs/img/node4.png diff --git a/packages/frontend/@n8n/i18n/package.json b/packages/frontend/@n8n/i18n/package.json new file mode 100644 index 0000000000..3d178e793d --- /dev/null +++ b/packages/frontend/@n8n/i18n/package.json @@ -0,0 +1,60 @@ +{ + "name": "@n8n/i18n", + "type": "module", + "version": "1.0.0", + "files": [ + "dist" + ], + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist/*.js", + "require": "./dist/*.cjs" + } + }, + "scripts": { + "dev": "vite", + "build": "pnpm run typecheck && tsup", + "preview": "vite preview", + "typecheck": "vue-tsc --noEmit", + "test": "vitest run", + "test:dev": "vitest --silent=false", + "lint": "eslint src --ext .js,.ts,.vue --quiet", + "lintfix": "eslint src --ext .js,.ts,.vue --fix", + "format": "biome format --write . && prettier --write . --ignore-path ../../../../.prettierignore", + "format:check": "biome ci . && prettier --check . --ignore-path ../../../../.prettierignore" + }, + "dependencies": { + "n8n-workflow": "workspace:*", + "vue-i18n": "catalog:frontend" + }, + "devDependencies": { + "@n8n/eslint-config": "workspace:*", + "@n8n/typescript-config": "workspace:*", + "@n8n/vitest-config": "workspace:*", + "@testing-library/jest-dom": "catalog:frontend", + "@testing-library/user-event": "catalog:frontend", + "@testing-library/vue": "catalog:frontend", + "@vitejs/plugin-vue": "catalog:frontend", + "@vue/tsconfig": "catalog:frontend", + "@vueuse/core": "catalog:frontend", + "vue": "catalog:frontend", + "tsup": "catalog:", + "typescript": "catalog:frontend", + "vite": "catalog:frontend", + "vitest": "catalog:frontend", + "vue-tsc": "catalog:frontend" + }, + "peerDependencies": { + "vue": "catalog:frontend" + }, + "license": "See LICENSE.md file in the root of the repository" +} diff --git a/packages/frontend/editor-ui/src/plugins/i18n/index.ts b/packages/frontend/@n8n/i18n/src/index.ts similarity index 90% rename from packages/frontend/editor-ui/src/plugins/i18n/index.ts rename to packages/frontend/@n8n/i18n/src/index.ts index d27245b5a7..5c477d1339 100644 --- a/packages/frontend/editor-ui/src/plugins/i18n/index.ts +++ b/packages/frontend/@n8n/i18n/src/index.ts @@ -1,13 +1,9 @@ -import axios from 'axios'; -import { createI18n } from 'vue-i18n'; -import { locale } from '@n8n/design-system'; +/* eslint-disable @typescript-eslint/no-this-alias */ import type { INodeProperties, INodePropertyCollection, INodePropertyOptions } from 'n8n-workflow'; +import { createI18n } from 'vue-i18n'; -import type { INodeTranslationHeaders } from '@/Interface'; -import { useUIStore } from '@/stores/ui.store'; -import { useNDVStore } from '@/stores/ndv.store'; -import { useRootStore } from '@n8n/stores/useRootStore'; import englishBaseText from './locales/en.json'; +import type { BaseTextKey, INodeTranslationHeaders } from './types'; import { deriveMiddleKey, isNestedInCollectionLike, @@ -15,6 +11,8 @@ import { insertOptionsAndValues, } from './utils'; +export * from './types'; + export const i18nInstance = createI18n({ locale: 'en', fallbackLocale: 'en', @@ -118,9 +116,7 @@ export class I18nClass { /** * Namespace for methods to render text in the credentials details modal. */ - credText() { - const uiStore = useUIStore(); - const credentialType = uiStore.activeCredentialType; + credText(credentialType: string | null) { const credentialPrefix = `n8n-nodes-base.credentials.${credentialType}`; const context = this; @@ -204,10 +200,8 @@ export class I18nClass { * Namespace for methods to render text in the node details view, * except for `eventTriggerDescription`. */ - nodeText() { - const ndvStore = useNDVStore(); - const activeNode = ndvStore.activeNode; - const nodeType = activeNode ? this.shortNodeType(activeNode.type) : ''; // unused in eventTriggerDescription + nodeText(activeNodeType?: string | null) { + const nodeType = activeNodeType ? this.shortNodeType(activeNodeType) : ''; // unused in eventTriggerDescription const initialKey = `n8n-nodes-base.nodes.${nodeType}.nodeView`; const context = this; @@ -355,10 +349,8 @@ export class I18nClass { }; } - localizeNodeName(nodeName: string, type: string) { - const isEnglishLocale = useRootStore().defaultLocale === 'en'; - - if (isEnglishLocale) return nodeName; + localizeNodeName(language: string, nodeName: string, type: string) { + if (language === 'en') return nodeName; const nodeTypeName = this.shortNodeType(type); @@ -377,12 +369,8 @@ const loadedLanguages = ['en']; async function setLanguage(language: string) { i18nInstance.global.locale = language as 'en'; - axios.defaults.headers.common['Accept-Language'] = language; document!.querySelector('html')!.setAttribute('lang', language); - // update n8n design system and element ui - await locale.use(language); - return language; } @@ -449,14 +437,6 @@ export function addHeaders(headers: INodeTranslationHeaders, language: string) { export const i18n: I18nClass = new I18nClass(); -// ---------------------------------- -// typings -// ---------------------------------- - -type GetBaseTextKey = T extends `_${string}` ? never : T; - -export type BaseTextKey = GetBaseTextKey; - -type GetCategoryName = T extends `nodeCreator.categoryNames.${infer C}` ? C : never; - -export type CategoryName = GetCategoryName; +export function useI18n() { + return i18n; +} diff --git a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/locales/en.json rename to packages/frontend/@n8n/i18n/src/locales/en.json diff --git a/packages/frontend/@n8n/i18n/src/shims.d.ts b/packages/frontend/@n8n/i18n/src/shims.d.ts new file mode 100644 index 0000000000..737ee1e318 --- /dev/null +++ b/packages/frontend/@n8n/i18n/src/shims.d.ts @@ -0,0 +1,5 @@ +/// + +export {}; + +declare module '*.json'; diff --git a/packages/frontend/@n8n/i18n/src/types.ts b/packages/frontend/@n8n/i18n/src/types.ts new file mode 100644 index 0000000000..d81a1bf33a --- /dev/null +++ b/packages/frontend/@n8n/i18n/src/types.ts @@ -0,0 +1,18 @@ +import type englishBaseText from './locales/en.json'; + +export type GetBaseTextKey = T extends `_${string}` ? never : T; + +export type BaseTextKey = GetBaseTextKey; + +export type GetCategoryName = T extends `nodeCreator.categoryNames.${infer C}` ? C : never; + +export type CategoryName = GetCategoryName; + +export interface INodeTranslationHeaders { + data: { + [key: string]: { + displayName: string; + description: string; + }; + }; +} diff --git a/packages/frontend/editor-ui/src/plugins/i18n/utils.ts b/packages/frontend/@n8n/i18n/src/utils.ts similarity index 100% rename from packages/frontend/editor-ui/src/plugins/i18n/utils.ts rename to packages/frontend/@n8n/i18n/src/utils.ts diff --git a/packages/frontend/@n8n/i18n/tsconfig.json b/packages/frontend/@n8n/i18n/tsconfig.json new file mode 100644 index 0000000000..63e10d0192 --- /dev/null +++ b/packages/frontend/@n8n/i18n/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@n8n/typescript-config/tsconfig.frontend.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": ".", + "outDir": "dist", + "types": ["vite/client", "vitest/globals"], + "isolatedModules": true, + "resolveJsonModule": true + }, + "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts", "tsup.config.ts"] +} diff --git a/packages/frontend/@n8n/i18n/tsup.config.ts b/packages/frontend/@n8n/i18n/tsup.config.ts new file mode 100644 index 0000000000..bff21e2550 --- /dev/null +++ b/packages/frontend/@n8n/i18n/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/*.d.ts', '!src/__tests__/**/*'], + format: ['cjs', 'esm'], + clean: true, + dts: true, + cjsInterop: true, + splitting: true, + sourcemap: true, +}); diff --git a/packages/frontend/@n8n/i18n/vite.config.ts b/packages/frontend/@n8n/i18n/vite.config.ts new file mode 100644 index 0000000000..784f3fb497 --- /dev/null +++ b/packages/frontend/@n8n/i18n/vite.config.ts @@ -0,0 +1,4 @@ +import { defineConfig, mergeConfig } from 'vite'; +import { vitestConfig } from '@n8n/vitest-config/frontend'; + +export default mergeConfig(defineConfig({}), vitestConfig); diff --git a/packages/frontend/@n8n/stores/tsup.config.ts b/packages/frontend/@n8n/stores/tsup.config.ts index 0d555b1ab0..bff21e2550 100644 --- a/packages/frontend/@n8n/stores/tsup.config.ts +++ b/packages/frontend/@n8n/stores/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ - entry: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/*.d.ts', '!src/__tests__**/*'], + entry: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/*.d.ts', '!src/__tests__/**/*'], format: ['cjs', 'esm'], clean: true, dts: true, diff --git a/packages/frontend/editor-ui/package.json b/packages/frontend/editor-ui/package.json index b03cfe919a..eaecfb409d 100644 --- a/packages/frontend/editor-ui/package.json +++ b/packages/frontend/editor-ui/package.json @@ -37,6 +37,7 @@ "@n8n/codemirror-lang-sql": "^1.0.2", "@n8n/composables": "workspace:*", "@n8n/design-system": "workspace:*", + "@n8n/i18n": "workspace:*", "@n8n/permissions": "workspace:*", "@n8n/stores": "workspace:*", "@n8n/utils": "workspace:*", @@ -84,7 +85,7 @@ "vue-agile": "^2.0.0", "vue-chartjs": "^5.2.0", "vue-github-button": "^3.1.3", - "vue-i18n": "^11.1.2", + "vue-i18n": "catalog:frontend", "vue-json-pretty": "2.2.4", "vue-markdown-render": "catalog:frontend", "vue-router": "catalog:frontend", diff --git a/packages/frontend/editor-ui/src/App.vue b/packages/frontend/editor-ui/src/App.vue index ebd33d65ec..9e3e774b81 100644 --- a/packages/frontend/editor-ui/src/App.vue +++ b/packages/frontend/editor-ui/src/App.vue @@ -9,7 +9,7 @@ import Modals from '@/components/Modals.vue'; import Telemetry from '@/components/Telemetry.vue'; import AskAssistantFloatingButton from '@/components/AskAssistant/Chat/AskAssistantFloatingButton.vue'; import AssistantsHub from '@/components/AskAssistant/AssistantsHub.vue'; -import { loadLanguage } from '@/plugins/i18n'; +import { loadLanguage } from '@n8n/i18n'; import { APP_MODALS_ELEMENT_ID, HIRING_BANNER, VIEWS } from '@/constants'; import { useRootStore } from '@n8n/stores/useRootStore'; import { useAssistantStore } from '@/stores/assistant.store'; @@ -19,6 +19,8 @@ import { useUsersStore } from '@/stores/users.store'; import { useSettingsStore } from '@/stores/settings.store'; import { useHistoryHelper } from '@/composables/useHistoryHelper'; import { useStyles } from './composables/useStyles'; +import { locale } from '@n8n/design-system'; +import axios from 'axios'; const route = useRoute(); const rootStore = useRootStore(); @@ -79,9 +81,15 @@ watch(route, (r) => { ); }); -watch(defaultLocale, (newLocale) => { - void loadLanguage(newLocale); -}); +watch( + defaultLocale, + (newLocale) => { + void loadLanguage(newLocale); + void locale.use(newLocale); + axios.defaults.headers.common['Accept-Language'] = newLocale; + }, + { immediate: true }, +);