From 4ed03c9f0e4e1a171283a17905644b5f77f1ff62 Mon Sep 17 00:00:00 2001 From: forehalo Date: Wed, 5 Feb 2025 12:30:18 +0000 Subject: [PATCH] feat(i18n): introduce server error i18n (#9953) close AF-2054 --- packages/backend/server/src/base/error/def.ts | 18 +- .../frontend/core/src/modules/cloud/error.ts | 9 +- .../core/src/modules/cloud/services/fetch.ts | 5 +- .../src/modules/cloud/services/graphql.ts | 17 +- packages/frontend/i18n/build.mjs | 59 --- packages/frontend/i18n/build.ts | 128 +++++ packages/frontend/i18n/package.json | 10 +- .../i18n/src/i18n-completenesses.json | 36 +- packages/frontend/i18n/src/i18n.gen.ts | 443 +++++++++++++++++- packages/frontend/i18n/src/resources/en.json | 88 +++- tools/cli/bin/runner.js | 4 + yarn.lock | 2 + 12 files changed, 709 insertions(+), 110 deletions(-) delete mode 100644 packages/frontend/i18n/build.mjs create mode 100644 packages/frontend/i18n/build.ts diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index 578dcbb296..550a8ffef9 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -1,7 +1,6 @@ import { STATUS_CODES } from 'node:http'; import { HttpStatus, Logger } from '@nestjs/common'; -import { capitalize } from 'lodash-es'; import { ClsServiceManager } from 'nestjs-cls'; export type UserFriendlyErrorBaseType = @@ -17,7 +16,7 @@ export type UserFriendlyErrorBaseType = | 'internal_server_error'; type ErrorArgType = 'string' | 'number' | 'boolean'; -type ErrorArgs = Record>; +type ErrorArgs = Record; export type UserFriendlyErrorOptions = { type: UserFriendlyErrorBaseType; @@ -152,25 +151,14 @@ export class UserFriendlyError extends Error { * @ObjectType() * export class XXXDataType { * @Field() - * + * [name]: [type]; * } */ function generateErrorArgs(name: string, args: ErrorArgs) { const typeName = `${name}DataType`; const lines = [`@ObjectType()`, `class ${typeName} {`]; Object.entries(args).forEach(([arg, fieldArgs]) => { - if (typeof fieldArgs === 'object') { - const subResult = generateErrorArgs( - name + 'Field' + capitalize(arg), - fieldArgs - ); - lines.unshift(subResult.def); - lines.push( - ` @Field(() => ${subResult.name}) ${arg}!: ${subResult.name};` - ); - } else { - lines.push(` @Field() ${arg}!: ${fieldArgs}`); - } + lines.push(` @Field() ${arg}!: ${fieldArgs}`); }); lines.push('}'); diff --git a/packages/frontend/core/src/modules/cloud/error.ts b/packages/frontend/core/src/modules/cloud/error.ts index 53201c9495..cd15096215 100644 --- a/packages/frontend/core/src/modules/cloud/error.ts +++ b/packages/frontend/core/src/modules/cloud/error.ts @@ -15,10 +15,11 @@ export function isNetworkError(error: Error): error is NetworkError { } export class BackendError extends Error { - constructor( - public readonly originError: UserFriendlyError, - public readonly status?: number - ) { + get status() { + return this.originError.status; + } + + constructor(public readonly originError: UserFriendlyError) { super(`Server error: ${originError.message}`); this.stack = originError.stack; } diff --git a/packages/frontend/core/src/modules/cloud/services/fetch.ts b/packages/frontend/core/src/modules/cloud/services/fetch.ts index 09ca356b5e..1b4dea04a9 100644 --- a/packages/frontend/core/src/modules/cloud/services/fetch.ts +++ b/packages/frontend/core/src/modules/cloud/services/fetch.ts @@ -78,10 +78,7 @@ export class FetchService extends Service { // ignore } } - throw new BackendError( - UserFriendlyError.fromAnyError(reason), - res.status - ); + throw new BackendError(UserFriendlyError.fromAnyError(reason)); } return res; }; diff --git a/packages/frontend/core/src/modules/cloud/services/graphql.ts b/packages/frontend/core/src/modules/cloud/services/graphql.ts index 16b68259d4..9ffcea9c80 100644 --- a/packages/frontend/core/src/modules/cloud/services/graphql.ts +++ b/packages/frontend/core/src/modules/cloud/services/graphql.ts @@ -1,8 +1,10 @@ import { gqlFetcherFactory, + GraphQLError, type GraphQLQuery, type QueryOptions, type QueryResponse, + UserFriendlyError, } from '@affine/graphql'; import { fromPromise, Service } from '@toeverything/infra'; import type { Observable } from 'rxjs'; @@ -37,12 +39,21 @@ export class GraphQLService extends Service { ): Promise> => { try { return await this.rawGql(options); - } catch (err) { - if (err instanceof BackendError && err.status === 403) { + } catch (anyError) { + let error = anyError; + + // NOTE(@forehalo): + // GraphQL error is not present by non-200 status code, but by responding `errors` fields in the body + // So it will never be `BackendError` originally. + if (anyError instanceof GraphQLError) { + error = new BackendError(UserFriendlyError.fromAnyError(anyError)); + } + + if (error instanceof BackendError && error.status === 403) { this.framework.get(AuthService).session.revalidate(); } - throw err; + throw error; } }; } diff --git a/packages/frontend/i18n/build.mjs b/packages/frontend/i18n/build.mjs deleted file mode 100644 index d63f59a6af..0000000000 --- a/packages/frontend/i18n/build.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { join, parse } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { runCli } from '@magic-works/i18n-codegen'; - -const isDev = process.argv.includes('--dev'); - -const pkgRoot = fileURLToPath(new URL('./', import.meta.url)); - -function calcCompletenesses() { - const resourcesDir = join(pkgRoot, 'src', 'resources'); - - const langs = readdirSync(resourcesDir) - .filter(file => file.endsWith('.json')) - .reduce((langs, file) => { - const filePath = `${resourcesDir}/${file}`; - const fileContent = JSON.parse(readFileSync(filePath, 'utf-8')); - langs[parse(file).name] = fileContent; - return langs; - }, {}); - - const base = Object.keys(langs.en).length; - - const completenesses = {}; - - for (const key in langs) { - const [langPart, variantPart] = key.split('-'); - - const completeness = Object.keys( - variantPart ? { ...langs[langPart], ...langs[key] } : langs[key] - ).length; - - completenesses[key] = Math.min( - Math.ceil(/* avoid 0% */ (completeness / base) * 100), - 100 - ); - } - - writeFileSync( - join(pkgRoot, 'src', 'i18n-completenesses.json'), - JSON.stringify(completenesses, null, 2) + '\n' - ); -} - -runCli( - { - config: fileURLToPath(new URL('./.i18n-codegen.json', import.meta.url)), - watch: isDev, - }, - error => { - console.error(error); - if (!isDev) { - process.exit(1); - } - } -); - -calcCompletenesses(); diff --git a/packages/frontend/i18n/build.ts b/packages/frontend/i18n/build.ts new file mode 100644 index 0000000000..f651f81979 --- /dev/null +++ b/packages/frontend/i18n/build.ts @@ -0,0 +1,128 @@ +import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { parse } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { Package } from '@affine-tools/utils/workspace'; +import { runCli } from '@magic-works/i18n-codegen'; + +const isDev = process.argv.includes('--dev'); + +const i18nPkg = new Package('@affine/i18n'); +const resourcesDir = i18nPkg.join('src', 'resources').toString(); + +function readResource(lang: string): Record { + const filePath = `${resourcesDir}/${lang}.json`; + const fileContent = JSON.parse(readFileSync(filePath, 'utf-8')); + return fileContent; +} + +function writeResource(lang: string, resource: Record) { + const filePath = `${resourcesDir}/${lang}.json`; + writeFileSync(filePath, JSON.stringify(resource, null, 2) + '\n'); +} + +function calcCompletenesses() { + const langs = readdirSync(resourcesDir) + .filter(file => file.endsWith('.json')) + .reduce( + (langs, file) => { + const lang = parse(file).name; + langs[lang] = readResource(lang); + return langs; + }, + {} as Record> + ); + + const base = Object.keys(langs.en).length; + + const completenesses = {}; + + for (const key in langs) { + const [langPart, variantPart] = key.split('-'); + + const completeness = Object.keys( + variantPart ? { ...langs[langPart], ...langs[key] } : langs[key] + ).length; + + completenesses[key] = Math.min( + Math.ceil(/* avoid 0% */ (completeness / base) * 100), + 100 + ); + } + + writeFileSync( + i18nPkg.join('src', 'i18n-completenesses.json').toString(), + JSON.stringify(completenesses, null, 2) + '\n' + ); +} + +function i18nnext() { + runCli( + { + config: fileURLToPath(new URL('./.i18n-codegen.json', import.meta.url)), + watch: isDev, + }, + error => { + console.error(error); + if (!isDev) { + process.exit(1); + } + } + ); +} + +async function appendErrorI18n() { + const server = new Package('@affine/server'); + const defFilePath = server.srcPath.join('base/error/def.ts'); + + if (!defFilePath.exists()) { + throw new Error( + `Can not find Server I18n error definition file. It's not placed at [${defFilePath.relativePath}].` + ); + } + + const { USER_FRIENDLY_ERRORS } = await import( + defFilePath.toFileUrl().toString() + ); + + if (!USER_FRIENDLY_ERRORS) { + throw new Error( + `Can not find Server I18n error definition file. It's not placed at [${defFilePath.relativePath}] with name [USER_FRIENDLY_ERRORS].` + ); + } + + const en = readResource('en'); + + Object.keys(en).forEach(key => { + if (key.startsWith('error.')) { + delete en[key]; + } + }); + + for (const key in USER_FRIENDLY_ERRORS) { + const def = USER_FRIENDLY_ERRORS[key] as { + type: string; + args?: Record; + message: string | ((args: any) => string); + }; + + en[`error.${key.toUpperCase()}`] = + typeof def.message === 'string' + ? def.message + : def.message( + Object.keys(def.args ?? {}).reduce( + (args, key) => { + args[key] = `{{${key}}}`; + return args; + }, + {} as Record + ) + ); + } + + writeResource('en', en); +} + +await appendErrorI18n(); +i18nnext(); +calcCompletenesses(); diff --git a/packages/frontend/i18n/package.json b/packages/frontend/i18n/package.json index 570c4774cd..2fbc60d8cb 100644 --- a/packages/frontend/i18n/package.json +++ b/packages/frontend/i18n/package.json @@ -1,5 +1,6 @@ { "name": "@affine/i18n", + "version": "0.19.0", "description": "", "type": "module", "private": true, @@ -8,8 +9,8 @@ ".": "./src/index.ts" }, "scripts": { - "build": "node build.mjs", - "dev": "node build.mjs --dev" + "build": "r build.ts", + "dev": "r build.ts --dev" }, "keywords": [], "repository": { @@ -26,8 +27,9 @@ "undici": "^7.1.0" }, "devDependencies": { + "@affine-tools/cli": "workspace:*", + "@affine-tools/utils": "workspace:*", "glob": "^11.0.0", "vitest": "3.0.5" - }, - "version": "0.19.0" + } } diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 3a60b19612..ac99656558 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -1,26 +1,26 @@ { - "ar": 98, + "ar": 92, "ca": 5, "da": 5, - "de": 98, - "el-GR": 98, + "de": 92, + "el-GR": 92, "en": 100, - "es-AR": 98, - "es-CL": 100, - "es": 98, - "fa": 98, - "fr": 98, + "es-AR": 92, + "es-CL": 94, + "es": 92, + "fa": 92, + "fr": 92, "hi": 2, - "it-IT": 98, + "it-IT": 92, "it": 1, - "ja": 98, - "ko": 71, - "pl": 98, - "pt-BR": 98, - "ru": 98, - "sv-SE": 98, - "uk": 98, + "ja": 92, + "ko": 67, + "pl": 92, + "pt-BR": 92, + "ru": 92, + "sv-SE": 92, + "uk": 92, "ur": 2, - "zh-Hans": 98, - "zh-Hant": 98 + "zh-Hans": 92, + "zh-Hant": 92 } diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index bf98546433..8bb176db98 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -5361,6 +5361,14 @@ export function useAFFiNEI18N(): { * `Once enabled, the header of page block will be displayed.` */ ["com.affine.settings.workspace.experimental-features.enable-page-block-header.description"](): string; + /** + * `Editor RTL` + */ + ["com.affine.settings.workspace.experimental-features.enable-editor-rtl.name"](): string; + /** + * `Once enabled, the editor will be displayed in RTL mode.` + */ + ["com.affine.settings.workspace.experimental-features.enable-editor-rtl.description"](): string; /** * `Only an owner can edit the workspace avatar and name. Changes will be shown for everyone.` */ @@ -5370,7 +5378,7 @@ export function useAFFiNEI18N(): { */ ["com.affine.settings.workspace.preferences"](): string; /** - * `Team's Team's Billing` + * `Team's Billing` */ ["com.affine.settings.workspace.billing"](): string; /** @@ -5550,7 +5558,7 @@ export function useAFFiNEI18N(): { */ ["com.affine.settings.workspace.backup.import.success.action"](): string; /** - * `Deleted {{date}} at {{time}}` + * `Deleted on {{date}} at {{time}}` */ ["com.affine.settings.workspace.backup.delete-at"](options: Readonly<{ date: string; @@ -6662,6 +6670,437 @@ export function useAFFiNEI18N(): { * `Remove template` */ ["com.affine.settings.workspace.template.remove"](): string; + /** + * `Unused blobs` + */ + ["com.affine.settings.workspace.storage.unused-blobs"](): string; + /** + * `No unused blobs` + */ + ["com.affine.settings.workspace.storage.unused-blobs.empty"](): string; + /** + * `Selected` + */ + ["com.affine.settings.workspace.storage.unused-blobs.selected"](): string; + /** + * `Delete blob files` + */ + ["com.affine.settings.workspace.storage.unused-blobs.delete.title"](): string; + /** + * `Are you sure you want to delete these blob files? This action cannot be undone. Make sure you no longer need them before proceeding.` + */ + ["com.affine.settings.workspace.storage.unused-blobs.delete.warning"](): string; + /** + * `An internal error occurred.` + */ + ["error.INTERNAL_SERVER_ERROR"](): string; + /** + * `Too many requests.` + */ + ["error.TOO_MANY_REQUEST"](): string; + /** + * `Resource not found.` + */ + ["error.NOT_FOUND"](): string; + /** + * `Query is too long, max length is {{max}}.` + */ + ["error.QUERY_TOO_LONG"](options: { + readonly max: string; + }): string; + /** + * `User not found.` + */ + ["error.USER_NOT_FOUND"](): string; + /** + * `User avatar not found.` + */ + ["error.USER_AVATAR_NOT_FOUND"](): string; + /** + * `This email has already been registered.` + */ + ["error.EMAIL_ALREADY_USED"](): string; + /** + * `You are trying to update your account email to the same as the old one.` + */ + ["error.SAME_EMAIL_PROVIDED"](): string; + /** + * `Wrong user email or password: {{email}}` + */ + ["error.WRONG_SIGN_IN_CREDENTIALS"](options: { + readonly email: string; + }): string; + /** + * `Unknown authentication provider {{name}}.` + */ + ["error.UNKNOWN_OAUTH_PROVIDER"](options: { + readonly name: string; + }): string; + /** + * `OAuth state expired, please try again.` + */ + ["error.OAUTH_STATE_EXPIRED"](): string; + /** + * `Invalid callback state parameter.` + */ + ["error.INVALID_OAUTH_CALLBACK_STATE"](): string; + /** + * `Missing query parameter `{{name}}`.` + */ + ["error.MISSING_OAUTH_QUERY_PARAMETER"](options: { + readonly name: string; + }): string; + /** + * `The third-party account has already been connected to another user.` + */ + ["error.OAUTH_ACCOUNT_ALREADY_CONNECTED"](): string; + /** + * `An invalid email provided: {{email}}` + */ + ["error.INVALID_EMAIL"](options: { + readonly email: string; + }): string; + /** + * `Password must be between {{min}} and {{max}} characters` + */ + ["error.INVALID_PASSWORD_LENGTH"](options: Readonly<{ + min: string; + max: string; + }>): string; + /** + * `Password is required.` + */ + ["error.PASSWORD_REQUIRED"](): string; + /** + * `You are trying to sign in by a different method than you signed up with.` + */ + ["error.WRONG_SIGN_IN_METHOD"](): string; + /** + * `You don't have early access permission. Visit https://community.affine.pro/c/insider-general/ for more information.` + */ + ["error.EARLY_ACCESS_REQUIRED"](): string; + /** + * `You are not allowed to sign up.` + */ + ["error.SIGN_UP_FORBIDDEN"](): string; + /** + * `The email token provided is not found.` + */ + ["error.EMAIL_TOKEN_NOT_FOUND"](): string; + /** + * `An invalid email token provided.` + */ + ["error.INVALID_EMAIL_TOKEN"](): string; + /** + * `The link has expired.` + */ + ["error.LINK_EXPIRED"](): string; + /** + * `You must sign in first to access this resource.` + */ + ["error.AUTHENTICATION_REQUIRED"](): string; + /** + * `You are not allowed to perform this action.` + */ + ["error.ACTION_FORBIDDEN"](): string; + /** + * `You do not have permission to access this resource.` + */ + ["error.ACCESS_DENIED"](): string; + /** + * `You must verify your email before accessing this resource.` + */ + ["error.EMAIL_VERIFICATION_REQUIRED"](): string; + /** + * `Space {{spaceId}} not found.` + */ + ["error.SPACE_NOT_FOUND"](options: { + readonly spaceId: string; + }): string; + /** + * `Member not found in Space {{spaceId}}.` + */ + ["error.MEMBER_NOT_FOUND_IN_SPACE"](options: { + readonly spaceId: string; + }): string; + /** + * `You should join in Space {{spaceId}} before broadcasting messages.` + */ + ["error.NOT_IN_SPACE"](options: { + readonly spaceId: string; + }): string; + /** + * `You have already joined in Space {{spaceId}}.` + */ + ["error.ALREADY_IN_SPACE"](options: { + readonly spaceId: string; + }): string; + /** + * `You do not have permission to access Space {{spaceId}}.` + */ + ["error.SPACE_ACCESS_DENIED"](options: { + readonly spaceId: string; + }): string; + /** + * `Owner of Space {{spaceId}} not found.` + */ + ["error.SPACE_OWNER_NOT_FOUND"](options: { + readonly spaceId: string; + }): string; + /** + * `Doc {{docId}} under Space {{spaceId}} not found.` + */ + ["error.DOC_NOT_FOUND"](options: Readonly<{ + docId: string; + spaceId: string; + }>): string; + /** + * `You do not have permission to access doc {{docId}} under Space {{spaceId}}.` + */ + ["error.DOC_ACCESS_DENIED"](options: Readonly<{ + docId: string; + spaceId: string; + }>): string; + /** + * `Your client with version {{version}} is rejected by remote sync server. Please upgrade to {{serverVersion}}.` + */ + ["error.VERSION_REJECTED"](options: Readonly<{ + version: string; + serverVersion: string; + }>): string; + /** + * `Invalid doc history timestamp provided.` + */ + ["error.INVALID_HISTORY_TIMESTAMP"](): string; + /** + * `History of {{docId}} at {{timestamp}} under Space {{spaceId}}.` + */ + ["error.DOC_HISTORY_NOT_FOUND"](options: Readonly<{ + docId: string; + timestamp: string; + spaceId: string; + }>): string; + /** + * `Blob {{blobId}} not found in Space {{spaceId}}.` + */ + ["error.BLOB_NOT_FOUND"](options: Readonly<{ + blobId: string; + spaceId: string; + }>): string; + /** + * `Expected to publish a page, not a Space.` + */ + ["error.EXPECT_TO_PUBLISH_PAGE"](): string; + /** + * `Expected to revoke a public page, not a Space.` + */ + ["error.EXPECT_TO_REVOKE_PUBLIC_PAGE"](): string; + /** + * `Page is not public.` + */ + ["error.PAGE_IS_NOT_PUBLIC"](): string; + /** + * `Failed to store doc updates.` + */ + ["error.FAILED_TO_SAVE_UPDATES"](): string; + /** + * `Failed to store doc snapshot.` + */ + ["error.FAILED_TO_UPSERT_SNAPSHOT"](): string; + /** + * `Unsupported subscription plan: {{plan}}.` + */ + ["error.UNSUPPORTED_SUBSCRIPTION_PLAN"](options: { + readonly plan: string; + }): string; + /** + * `Failed to create checkout session.` + */ + ["error.FAILED_TO_CHECKOUT"](): string; + /** + * `Invalid checkout parameters provided.` + */ + ["error.INVALID_CHECKOUT_PARAMETERS"](): string; + /** + * `You have already subscribed to the {{plan}} plan.` + */ + ["error.SUBSCRIPTION_ALREADY_EXISTS"](options: { + readonly plan: string; + }): string; + /** + * `Invalid subscription parameters provided.` + */ + ["error.INVALID_SUBSCRIPTION_PARAMETERS"](): string; + /** + * `You didn't subscribe to the {{plan}} plan.` + */ + ["error.SUBSCRIPTION_NOT_EXISTS"](options: { + readonly plan: string; + }): string; + /** + * `Your subscription has already been canceled.` + */ + ["error.SUBSCRIPTION_HAS_BEEN_CANCELED"](): string; + /** + * `Your subscription has not been canceled.` + */ + ["error.SUBSCRIPTION_HAS_NOT_BEEN_CANCELED"](): string; + /** + * `Your subscription has expired.` + */ + ["error.SUBSCRIPTION_EXPIRED"](): string; + /** + * `Your subscription has already been in {{recurring}} recurring state.` + */ + ["error.SAME_SUBSCRIPTION_RECURRING"](options: { + readonly recurring: string; + }): string; + /** + * `Failed to create customer portal session.` + */ + ["error.CUSTOMER_PORTAL_CREATE_FAILED"](): string; + /** + * `You are trying to access a unknown subscription plan.` + */ + ["error.SUBSCRIPTION_PLAN_NOT_FOUND"](): string; + /** + * `You cannot update an onetime payment subscription.` + */ + ["error.CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION"](): string; + /** + * `A workspace is required to checkout for team subscription.` + */ + ["error.WORKSPACE_ID_REQUIRED_FOR_TEAM_SUBSCRIPTION"](): string; + /** + * `Workspace id is required to update team subscription.` + */ + ["error.WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION"](): string; + /** + * `Copilot session not found.` + */ + ["error.COPILOT_SESSION_NOT_FOUND"](): string; + /** + * `Copilot session has been deleted.` + */ + ["error.COPILOT_SESSION_DELETED"](): string; + /** + * `No copilot provider available.` + */ + ["error.NO_COPILOT_PROVIDER_AVAILABLE"](): string; + /** + * `Failed to generate text.` + */ + ["error.COPILOT_FAILED_TO_GENERATE_TEXT"](): string; + /** + * `Failed to create chat message.` + */ + ["error.COPILOT_FAILED_TO_CREATE_MESSAGE"](): string; + /** + * `Unsplash is not configured.` + */ + ["error.UNSPLASH_IS_NOT_CONFIGURED"](): string; + /** + * `Action has been taken, no more messages allowed.` + */ + ["error.COPILOT_ACTION_TAKEN"](): string; + /** + * `Copilot message {{messageId}} not found.` + */ + ["error.COPILOT_MESSAGE_NOT_FOUND"](options: { + readonly messageId: string; + }): string; + /** + * `Copilot prompt {{name}} not found.` + */ + ["error.COPILOT_PROMPT_NOT_FOUND"](options: { + readonly name: string; + }): string; + /** + * `Copilot prompt is invalid.` + */ + ["error.COPILOT_PROMPT_INVALID"](): string; + /** + * `Provider {{provider}} failed with {{kind}} error: {{message}}` + */ + ["error.COPILOT_PROVIDER_SIDE_ERROR"](options: Readonly<{ + provider: string; + kind: string; + message: string; + }>): string; + /** + * `You have exceeded your blob storage quota.` + */ + ["error.BLOB_QUOTA_EXCEEDED"](): string; + /** + * `You have exceeded your workspace member quota.` + */ + ["error.MEMBER_QUOTA_EXCEEDED"](): string; + /** + * `You have reached the limit of actions in this workspace, please upgrade your plan.` + */ + ["error.COPILOT_QUOTA_EXCEEDED"](): string; + /** + * `Runtime config {{key}} not found.` + */ + ["error.RUNTIME_CONFIG_NOT_FOUND"](options: { + readonly key: string; + }): string; + /** + * `Invalid runtime config type for '{{key}}', want '{{want}}', but get {{get}}.` + */ + ["error.INVALID_RUNTIME_CONFIG_TYPE"](options: Readonly<{ + key: string; + want: string; + get: string; + }>): string; + /** + * `Mailer service is not configured.` + */ + ["error.MAILER_SERVICE_IS_NOT_CONFIGURED"](): string; + /** + * `Cannot delete all admin accounts.` + */ + ["error.CANNOT_DELETE_ALL_ADMIN_ACCOUNT"](): string; + /** + * `Cannot delete own account.` + */ + ["error.CANNOT_DELETE_OWN_ACCOUNT"](): string; + /** + * `Captcha verification failed.` + */ + ["error.CAPTCHA_VERIFICATION_FAILED"](): string; + /** + * `Invalid session id to generate license key.` + */ + ["error.INVALID_LICENSE_SESSION_ID"](): string; + /** + * `License key has been revealed. Please check your mail box of the one provided during checkout.` + */ + ["error.LICENSE_REVEALED"](): string; + /** + * `Workspace already has a license applied.` + */ + ["error.WORKSPACE_LICENSE_ALREADY_EXISTS"](): string; + /** + * `License not found.` + */ + ["error.LICENSE_NOT_FOUND"](): string; + /** + * `Invalid license to activate.` + */ + ["error.INVALID_LICENSE_TO_ACTIVATE"](): string; + /** + * `Invalid license update params. {{reason}}` + */ + ["error.INVALID_LICENSE_UPDATE_PARAMS"](options: { + readonly reason: string; + }): string; + /** + * `You cannot downgrade the workspace from team workspace because there are more than {{limit}} members that are currently active.` + */ + ["error.WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE"](options: { + readonly limit: string; + }): string; } { const { t } = useTranslation(); return useMemo(() => createProxy((key) => t.bind(null, key)), [t]); } function createComponent(i18nKey: string) { return (props) => createElement(Trans, { i18nKey, shouldUnescape: true, ...props }); diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index b89f6b649b..d3c929f194 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1666,5 +1666,91 @@ "com.affine.settings.workspace.storage.unused-blobs.empty": "No unused blobs", "com.affine.settings.workspace.storage.unused-blobs.selected": "Selected", "com.affine.settings.workspace.storage.unused-blobs.delete.title": "Delete blob files", - "com.affine.settings.workspace.storage.unused-blobs.delete.warning": "Are you sure you want to delete these blob files? This action cannot be undone. Make sure you no longer need them before proceeding." + "com.affine.settings.workspace.storage.unused-blobs.delete.warning": "Are you sure you want to delete these blob files? This action cannot be undone. Make sure you no longer need them before proceeding.", + "error.INTERNAL_SERVER_ERROR": "An internal error occurred.", + "error.TOO_MANY_REQUEST": "Too many requests.", + "error.NOT_FOUND": "Resource not found.", + "error.QUERY_TOO_LONG": "Query is too long, max length is {{max}}.", + "error.USER_NOT_FOUND": "User not found.", + "error.USER_AVATAR_NOT_FOUND": "User avatar not found.", + "error.EMAIL_ALREADY_USED": "This email has already been registered.", + "error.SAME_EMAIL_PROVIDED": "You are trying to update your account email to the same as the old one.", + "error.WRONG_SIGN_IN_CREDENTIALS": "Wrong user email or password: {{email}}", + "error.UNKNOWN_OAUTH_PROVIDER": "Unknown authentication provider {{name}}.", + "error.OAUTH_STATE_EXPIRED": "OAuth state expired, please try again.", + "error.INVALID_OAUTH_CALLBACK_STATE": "Invalid callback state parameter.", + "error.MISSING_OAUTH_QUERY_PARAMETER": "Missing query parameter `{{name}}`.", + "error.OAUTH_ACCOUNT_ALREADY_CONNECTED": "The third-party account has already been connected to another user.", + "error.INVALID_EMAIL": "An invalid email provided: {{email}}", + "error.INVALID_PASSWORD_LENGTH": "Password must be between {{min}} and {{max}} characters", + "error.PASSWORD_REQUIRED": "Password is required.", + "error.WRONG_SIGN_IN_METHOD": "You are trying to sign in by a different method than you signed up with.", + "error.EARLY_ACCESS_REQUIRED": "You don't have early access permission. Visit https://community.affine.pro/c/insider-general/ for more information.", + "error.SIGN_UP_FORBIDDEN": "You are not allowed to sign up.", + "error.EMAIL_TOKEN_NOT_FOUND": "The email token provided is not found.", + "error.INVALID_EMAIL_TOKEN": "An invalid email token provided.", + "error.LINK_EXPIRED": "The link has expired.", + "error.AUTHENTICATION_REQUIRED": "You must sign in first to access this resource.", + "error.ACTION_FORBIDDEN": "You are not allowed to perform this action.", + "error.ACCESS_DENIED": "You do not have permission to access this resource.", + "error.EMAIL_VERIFICATION_REQUIRED": "You must verify your email before accessing this resource.", + "error.SPACE_NOT_FOUND": "Space {{spaceId}} not found.", + "error.MEMBER_NOT_FOUND_IN_SPACE": "Member not found in Space {{spaceId}}.", + "error.NOT_IN_SPACE": "You should join in Space {{spaceId}} before broadcasting messages.", + "error.ALREADY_IN_SPACE": "You have already joined in Space {{spaceId}}.", + "error.SPACE_ACCESS_DENIED": "You do not have permission to access Space {{spaceId}}.", + "error.SPACE_OWNER_NOT_FOUND": "Owner of Space {{spaceId}} not found.", + "error.DOC_NOT_FOUND": "Doc {{docId}} under Space {{spaceId}} not found.", + "error.DOC_ACCESS_DENIED": "You do not have permission to access doc {{docId}} under Space {{spaceId}}.", + "error.VERSION_REJECTED": "Your client with version {{version}} is rejected by remote sync server. Please upgrade to {{serverVersion}}.", + "error.INVALID_HISTORY_TIMESTAMP": "Invalid doc history timestamp provided.", + "error.DOC_HISTORY_NOT_FOUND": "History of {{docId}} at {{timestamp}} under Space {{spaceId}}.", + "error.BLOB_NOT_FOUND": "Blob {{blobId}} not found in Space {{spaceId}}.", + "error.EXPECT_TO_PUBLISH_PAGE": "Expected to publish a page, not a Space.", + "error.EXPECT_TO_REVOKE_PUBLIC_PAGE": "Expected to revoke a public page, not a Space.", + "error.PAGE_IS_NOT_PUBLIC": "Page is not public.", + "error.FAILED_TO_SAVE_UPDATES": "Failed to store doc updates.", + "error.FAILED_TO_UPSERT_SNAPSHOT": "Failed to store doc snapshot.", + "error.UNSUPPORTED_SUBSCRIPTION_PLAN": "Unsupported subscription plan: {{plan}}.", + "error.FAILED_TO_CHECKOUT": "Failed to create checkout session.", + "error.INVALID_CHECKOUT_PARAMETERS": "Invalid checkout parameters provided.", + "error.SUBSCRIPTION_ALREADY_EXISTS": "You have already subscribed to the {{plan}} plan.", + "error.INVALID_SUBSCRIPTION_PARAMETERS": "Invalid subscription parameters provided.", + "error.SUBSCRIPTION_NOT_EXISTS": "You didn't subscribe to the {{plan}} plan.", + "error.SUBSCRIPTION_HAS_BEEN_CANCELED": "Your subscription has already been canceled.", + "error.SUBSCRIPTION_HAS_NOT_BEEN_CANCELED": "Your subscription has not been canceled.", + "error.SUBSCRIPTION_EXPIRED": "Your subscription has expired.", + "error.SAME_SUBSCRIPTION_RECURRING": "Your subscription has already been in {{recurring}} recurring state.", + "error.CUSTOMER_PORTAL_CREATE_FAILED": "Failed to create customer portal session.", + "error.SUBSCRIPTION_PLAN_NOT_FOUND": "You are trying to access a unknown subscription plan.", + "error.CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION": "You cannot update an onetime payment subscription.", + "error.WORKSPACE_ID_REQUIRED_FOR_TEAM_SUBSCRIPTION": "A workspace is required to checkout for team subscription.", + "error.WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION": "Workspace id is required to update team subscription.", + "error.COPILOT_SESSION_NOT_FOUND": "Copilot session not found.", + "error.COPILOT_SESSION_DELETED": "Copilot session has been deleted.", + "error.NO_COPILOT_PROVIDER_AVAILABLE": "No copilot provider available.", + "error.COPILOT_FAILED_TO_GENERATE_TEXT": "Failed to generate text.", + "error.COPILOT_FAILED_TO_CREATE_MESSAGE": "Failed to create chat message.", + "error.UNSPLASH_IS_NOT_CONFIGURED": "Unsplash is not configured.", + "error.COPILOT_ACTION_TAKEN": "Action has been taken, no more messages allowed.", + "error.COPILOT_MESSAGE_NOT_FOUND": "Copilot message {{messageId}} not found.", + "error.COPILOT_PROMPT_NOT_FOUND": "Copilot prompt {{name}} not found.", + "error.COPILOT_PROMPT_INVALID": "Copilot prompt is invalid.", + "error.COPILOT_PROVIDER_SIDE_ERROR": "Provider {{provider}} failed with {{kind}} error: {{message}}", + "error.BLOB_QUOTA_EXCEEDED": "You have exceeded your blob storage quota.", + "error.MEMBER_QUOTA_EXCEEDED": "You have exceeded your workspace member quota.", + "error.COPILOT_QUOTA_EXCEEDED": "You have reached the limit of actions in this workspace, please upgrade your plan.", + "error.RUNTIME_CONFIG_NOT_FOUND": "Runtime config {{key}} not found.", + "error.INVALID_RUNTIME_CONFIG_TYPE": "Invalid runtime config type for '{{key}}', want '{{want}}', but get {{get}}.", + "error.MAILER_SERVICE_IS_NOT_CONFIGURED": "Mailer service is not configured.", + "error.CANNOT_DELETE_ALL_ADMIN_ACCOUNT": "Cannot delete all admin accounts.", + "error.CANNOT_DELETE_OWN_ACCOUNT": "Cannot delete own account.", + "error.CAPTCHA_VERIFICATION_FAILED": "Captcha verification failed.", + "error.INVALID_LICENSE_SESSION_ID": "Invalid session id to generate license key.", + "error.LICENSE_REVEALED": "License key has been revealed. Please check your mail box of the one provided during checkout.", + "error.WORKSPACE_LICENSE_ALREADY_EXISTS": "Workspace already has a license applied.", + "error.LICENSE_NOT_FOUND": "License not found.", + "error.INVALID_LICENSE_TO_ACTIVATE": "Invalid license to activate.", + "error.INVALID_LICENSE_UPDATE_PARAMS": "Invalid license update params. {{reason}}", + "error.WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE": "You cannot downgrade the workspace from team workspace because there are more than {{limit}} members that are currently active." } diff --git a/tools/cli/bin/runner.js b/tools/cli/bin/runner.js index 2c7cd20e1c..94f22947e3 100755 --- a/tools/cli/bin/runner.js +++ b/tools/cli/bin/runner.js @@ -28,6 +28,10 @@ const lookups = []; */ let scriptLocation; for (const location of fileLocationCandidates) { + if (scriptLocation) { + break; + } + const fileCandidates = [file, `${file}.js`, `${file}.ts`]; for (const candidate of fileCandidates) { const candidateLocation = join(location, candidate); diff --git a/yarn.lock b/yarn.lock index 7ed9301a79..74856a4f2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -585,6 +585,8 @@ __metadata: version: 0.0.0-use.local resolution: "@affine/i18n@workspace:packages/frontend/i18n" dependencies: + "@affine-tools/cli": "workspace:*" + "@affine-tools/utils": "workspace:*" "@affine/debug": "workspace:*" "@magic-works/i18n-codegen": "npm:^0.6.1" dayjs: "npm:^1.11.13"