From 9667126dcc03f708106fa7581fabbd69a64be3e3 Mon Sep 17 00:00:00 2001 From: David Hordiienko Date: Thu, 27 Jun 2024 14:33:42 +0300 Subject: [PATCH] add: translated error warnings --- backend/src/lib/common-schemas.ts | 43 +++++++++++++------ backend/src/lib/utils.ts | 6 +++ backend/src/modules/general/schema.ts | 3 +- backend/src/modules/memberships/schema.ts | 3 +- backend/src/modules/organizations/schema.ts | 6 ++- backend/src/modules/requests/schema.ts | 6 ++- .../src/modules/users/invite-email-form.tsx | 3 +- .../src/modules/users/invite-search-form.tsx | 3 +- locales/en/backend.json | 14 +++++- 9 files changed, 66 insertions(+), 21 deletions(-) diff --git a/backend/src/lib/common-schemas.ts b/backend/src/lib/common-schemas.ts index fa84cf28b..51ff1888d 100644 --- a/backend/src/lib/common-schemas.ts +++ b/backend/src/lib/common-schemas.ts @@ -1,5 +1,6 @@ import { config } from 'config'; import { z } from 'zod'; +import { t } from './utils'; export const passwordSchema = z.string().min(8).max(100); @@ -44,8 +45,16 @@ export const paginationQuerySchema = z.object({ q: z.string().optional(), sort: z.enum(['createdAt']).default('createdAt').optional(), order: z.enum(['asc', 'desc']).default('asc').optional(), - offset: z.string().default('0').optional().refine(offsetRefine, 'Must be number greater or equal to 0'), - limit: z.string().default('50').optional().refine(limitRefine, 'Must be number greater than 0'), + offset: z + .string() + .default('0') + .optional() + .refine(offsetRefine, { message: t('invalid.min_length_greater_or_eq', { length: 0 }) }), + limit: z + .string() + .default('50') + .optional() + .refine(limitRefine, { message: t('invalid.min_length_greater', { length: 0 }) }), }); export const idsQuerySchema = z.object({ @@ -56,10 +65,9 @@ export const validSlugSchema = z .string() .min(2) .max(100) - .refine( - (s) => /^[a-z0-9]+(-{0,3}[a-z0-9]+)*$/i.test(s), - 'Slug may only contain alphanumeric characters or up to three hyphens, and cannot begin or end with a hyphen.', - ) + .refine((s) => /^[a-z0-9]+(-{0,3}[a-z0-9]+)*$/i.test(s), { + message: t('invalid.slug'), + }) .transform((str) => str.toLowerCase().trim()); export const validDomainsSchema = z @@ -68,10 +76,9 @@ export const validDomainsSchema = z .string() .min(4) .max(100) - .refine( - (s) => /^[a-z0-9].*[a-z0-9]$/i.test(s) && s.includes('.'), - 'Domain must not contain @, no special chars and at least one dot (.) in between.', - ) + .refine((s) => /^[a-z0-9].*[a-z0-9]$/i.test(s) && s.includes('.'), { + message: t('invalid.domain'), + }) .transform((str) => str.toLowerCase().trim()), ) .optional(); @@ -91,18 +98,26 @@ export const membershipsCountSchema = z.object({ export const imageUrlSchema = z .string() .url() - .refine((url) => new URL(url).search === '', 'Search params not allowed'); + .refine((url) => new URL(url).search === '', { + message: t('invalid.image_url'), + }); export const nameSchema = z .string() .min(2) .max(100) - .refine((s) => /^[a-z0-9 ,.'-]+$/i.test(s), "Name may only contain letters, numbers, spaces and these characters: ,.'-"); + .refine((s) => /^[a-z0-9 ,.'-]+$/i.test(s), { + message: t('invalid.name'), + }); export const colorSchema = z .string() .min(3) .max(7) - .regex(/^#(?:[0-9a-fA-F]{3}){1,2}$/, 'Color may only contain letters, numbers & starts with #'); + .regex(/^#(?:[0-9a-fA-F]{3}){1,2}$/, { + message: t('invalid.color'), + }); -export const validUrlSchema = z.string().refine((url: string) => url.startsWith('https'), 'URL must start with https://'); +export const validUrlSchema = z.string().refine((url: string) => url.startsWith('https'), { + message: t('invalid.url'), +}); diff --git a/backend/src/lib/utils.ts b/backend/src/lib/utils.ts index 0d10ec8b8..5fef23985 100644 --- a/backend/src/lib/utils.ts +++ b/backend/src/lib/utils.ts @@ -1,5 +1,7 @@ import { env } from 'env'; import { sign } from 'hono/jwt'; +import { i18n } from './i18n'; +import { config } from 'config'; interface GenerateTokenOptions { userId: string; @@ -24,3 +26,7 @@ export const generateElectricJWTToken = async ({ userId }: GenerateTokenOptions) 'ES256', ); }; + +export const { t } = i18n.cloneInstance({ + lng: config.defaultLanguage, +}); diff --git a/backend/src/modules/general/schema.ts b/backend/src/modules/general/schema.ts index 2ecf2ab9a..546494af1 100644 --- a/backend/src/modules/general/schema.ts +++ b/backend/src/modules/general/schema.ts @@ -15,6 +15,7 @@ import { } from '../../lib/common-schemas'; import { membershipInfoSchema } from '../memberships/schema'; import { userSchema } from '../users/schema'; +import { t } from '../../lib/utils'; export const publicCountsSchema = z.object({ users: z.number(), @@ -33,7 +34,7 @@ export const checkTokenSchema = z.object({ }); export const inviteBodySchema = z.object({ - emails: userSchema.shape.email.array().min(1), + emails: userSchema.shape.email.array().min(1, { message: t('invalid.min_items', { items_count: 'one', item: 'email' }) }), role: userSchema.shape.role, }); diff --git a/backend/src/modules/memberships/schema.ts b/backend/src/modules/memberships/schema.ts index 506b9ab91..005b11755 100644 --- a/backend/src/modules/memberships/schema.ts +++ b/backend/src/modules/memberships/schema.ts @@ -4,6 +4,7 @@ import { createSelectSchema } from 'drizzle-zod'; import { membershipsTable } from '../../db/schema/memberships'; import { contextEntityTypeSchema, idOrSlugSchema, idSchema, idsQuerySchema } from '../../lib/common-schemas'; import { userSchema } from '../users/schema'; +import { t } from '../../lib/utils'; const membershipTableSchema = createSelectSchema(membershipsTable); @@ -15,7 +16,7 @@ export const membershipSchema = membershipTableSchema.extend({ }); export const createMembershipBodySchema = z.object({ - emails: userSchema.shape.email.array().min(1), + emails: userSchema.shape.email.array().min(1, { message: t('invalid.min_items', { items_count: 'one', item: 'email' }) }), role: membershipSchema.shape.role, }); diff --git a/backend/src/modules/organizations/schema.ts b/backend/src/modules/organizations/schema.ts index 1199af6f1..55abd0877 100644 --- a/backend/src/modules/organizations/schema.ts +++ b/backend/src/modules/organizations/schema.ts @@ -12,6 +12,7 @@ import { validUrlSchema, } from '../../lib/common-schemas'; import { membershipInfoSchema } from '../memberships/schema'; +import { t } from '../../lib/utils'; export const organizationSchema = z.object({ ...createSelectSchema(organizationsTable).shape, @@ -33,7 +34,10 @@ export const updateOrganizationBodySchema = createInsertSchema(organizationsTabl slug: validSlugSchema, name: nameSchema, shortName: nameSchema, - languages: z.array(z.string()).min(1).optional(), + languages: z + .array(z.string()) + .min(1, { message: t('invalid.min_items', { items_count: 'one', item: 'language' }) }) + .optional(), emailDomains: validDomainsSchema, authStrategies: z.array(z.string()).optional(), websiteUrl: validUrlSchema, diff --git a/backend/src/modules/requests/schema.ts b/backend/src/modules/requests/schema.ts index 1549524de..00d963632 100644 --- a/backend/src/modules/requests/schema.ts +++ b/backend/src/modules/requests/schema.ts @@ -3,11 +3,15 @@ import { z } from 'zod'; import { createSelectSchema } from 'drizzle-zod'; import { requestsTable } from '../../db/schema/requests'; import { paginationQuerySchema } from '../../lib/common-schemas'; +import { t } from '../../lib/utils'; const requestsTableSchema = createSelectSchema(requestsTable); export const requestsSchema = z.object({ - email: z.string().min(1).email(), + email: z + .string() + .email(t('invalid.email')) + .min(1, { message: t('required') }), type: requestsTableSchema.shape.type, }); diff --git a/frontend/src/modules/users/invite-email-form.tsx b/frontend/src/modules/users/invite-email-form.tsx index fb3642d11..7c2ff33f6 100644 --- a/frontend/src/modules/users/invite-email-form.tsx +++ b/frontend/src/modules/users/invite-email-form.tsx @@ -20,6 +20,7 @@ import { Button } from '~/modules/ui/button'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '~/modules/ui/form'; import type { EntityPage } from '~/types'; import { idOrSlugSchema } from 'backend/lib/common-schemas'; +import { t } from 'backend/lib/utils'; interface Props { entity?: EntityPage; @@ -29,7 +30,7 @@ interface Props { } const formSchema = z.object({ - emails: z.array(z.string().email('Invalid email')).min(1), + emails: z.array(z.string().email(t('invalid.email'))).min(1, { message: t('invalid.min_items', { items_count: 'one', item: 'email' }) }), role: z.enum(config.rolesByType.allRoles), idOrSlug: idOrSlugSchema.optional(), }); diff --git a/frontend/src/modules/users/invite-search-form.tsx b/frontend/src/modules/users/invite-search-form.tsx index 7860b2878..b6da95092 100644 --- a/frontend/src/modules/users/invite-search-form.tsx +++ b/frontend/src/modules/users/invite-search-form.tsx @@ -18,6 +18,7 @@ import { Button } from '~/modules/ui/button'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '~/modules/ui/form'; import type { EntityPage } from '~/types'; import { QueryCombobox } from '~/modules/common/query-combobox'; +import { t } from 'backend/lib/utils'; interface Props { entity?: EntityPage; @@ -26,7 +27,7 @@ interface Props { } const formSchema = z.object({ - emails: z.array(z.string().email('Invalid email')).min(1), + emails: z.array(z.string().email(t('invalid.email'))).min(1, { message: t('invalid.min_items', { items_count: 'one', item: 'email' }) }), role: z.enum(config.rolesByType.entityRoles).optional(), idOrSlug: idOrSlugSchema.optional(), }); diff --git a/locales/en/backend.json b/locales/en/backend.json index 4b65f8b02..8c9dac2f6 100644 --- a/locales/en/backend.json +++ b/locales/en/backend.json @@ -17,5 +17,17 @@ "email.reset_password_text_3": "Ignore this email if you don't want to change your password or didn't request it.", "email.reset_password_text_2": "This link will expire in 3 hours. To get a new password reset link, visit:", "email.reset_password_text_1": "A password reset link has been requested for your Cella account. If this was you, you can set a new password here:", - "email.hi": "Hi" + "email.hi": "Hi", + "invalid.min_items": "Please add at least {{items_count}} {{item}} to the list.", + "invalid.min_length_greater": "Must be greater than {{length}}", + "invalid.min_length_greater_or_eq": "Must be greater or equal to {{length}}", + "invalid.max_length": "{{prefix}} be at most {{length}} characters.", + "invalid.name": "Name may only contain letters, numbers, spaces and these characters: ,.'-", + "invalid.url": "URL must start with https://", + "invalid.image_url": "Search params not allowed", + "invalid.color": "Color may only contain letters, numbers & starts with #", + "invalid.domain": "Domain must not contain @, no special chars and at least one dot (.) in between.", + "invalid.slug": "Slug may only contain alphanumeric characters or up to three hyphens, and cannot begin or end with a hyphen.", + "invalid.email": "Invalid email address.", + "required": "This field is required." }