feat(i18n): introduce server error i18n (#9953)

close AF-2054
This commit is contained in:
forehalo
2025-02-05 12:30:18 +00:00
parent 4a943d854e
commit 4ed03c9f0e
12 changed files with 709 additions and 110 deletions

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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<QueryResponse<Query>> => {
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;
}
};
}

View File

@@ -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();

View File

@@ -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<string, string> {
const filePath = `${resourcesDir}/${lang}.json`;
const fileContent = JSON.parse(readFileSync(filePath, 'utf-8'));
return fileContent;
}
function writeResource(lang: string, resource: Record<string, string>) {
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<string, Record<string, string>>
);
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<string, any>;
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<string, string>
)
);
}
writeResource('en', en);
}
await appendErrorI18n();
i18nnext();
calcCompletenesses();

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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 });

View File

@@ -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."
}