diff --git a/.changeset/i18n-web-translations.md b/.changeset/i18n-web-translations.md new file mode 100644 index 000000000..853edc0d1 --- /dev/null +++ b/.changeset/i18n-web-translations.md @@ -0,0 +1,23 @@ +--- +"@baseapp-frontend/components": minor +"@baseapp-frontend/design-system": minor +"@baseapp-frontend/utils": minor +"@baseapp-frontend/authentication": minor +"@baseapp-frontend/i18n": minor +--- + +Translate every web string to English, Spanish and Portuguese through react-intl. + +- `components` and `design-system` web UI render all text via `formatMessage` with ids + namespaced per module; catalogs ship in `@baseapp-frontend/{components,design-system}/locales`. + Native code and shared `common/` code used by native are unchanged. +- Default prop strings (e.g. `cancelText`, `placeholder`, `title`) are now translated fallbacks; + passing the prop still overrides them. +- `utils`: `getZodMessages(intl)`, `ZOD_MESSAGE_DESCRIPTORS` and the `IntlFormatter` type. +- `authentication`: intl factories next to every validation schema + (`getLoginValidationSchema`, `getSignUpValidationSchema`, `getCodeValidationSchema`, …) and a + new `mfaFormOptions` option on `useLogin`. Existing schema constants are unchanged. +- `i18n`: `IntlProviderWrapper` accepts `additionalMessagesByLocale`; the app-specific + `settings.*` messages moved out of the package catalog. +- Web components that render text now require an `IntlProvider` ancestor (the package test + providers include one). diff --git a/packages/authentication/modules/access/useChangePassword/constants.ts b/packages/authentication/modules/access/useChangePassword/constants.ts index 738511330..f5e1d7f2c 100644 --- a/packages/authentication/modules/access/useChangePassword/constants.ts +++ b/packages/authentication/modules/access/useChangePassword/constants.ts @@ -1,21 +1,33 @@ -import { PASSWORD_REGEX, ZOD_MESSAGE } from '@baseapp-frontend/utils' +import { + type IntlFormatter, + PASSWORD_REGEX, + ZOD_MESSAGE, + type ZodMessages, + getZodMessages, +} from '@baseapp-frontend/utils' import { z } from 'zod' import type { ChangePasswordForm } from './types' -export const DEFAULT_VALIDATION_SCHEMA = z - .object({ - currentPassword: z.string().nonempty(ZOD_MESSAGE.required), - newPassword: z.string().min(1, ZOD_MESSAGE.required).regex(PASSWORD_REGEX, { - message: ZOD_MESSAGE.password, - }), - confirmNewPassword: z.string().nonempty(ZOD_MESSAGE.required), - }) - .refine(({ confirmNewPassword, newPassword }) => newPassword === confirmNewPassword, { - message: ZOD_MESSAGE.passwordDoNotMatch, - path: ['confirmNewPassword'], - }) +const createValidationSchema = (messages: ZodMessages) => + z + .object({ + currentPassword: z.string().nonempty(messages.required), + newPassword: z.string().min(1, messages.required).regex(PASSWORD_REGEX, { + message: messages.password, + }), + confirmNewPassword: z.string().nonempty(messages.required), + }) + .refine(({ confirmNewPassword, newPassword }) => newPassword === confirmNewPassword, { + message: messages.passwordDoNotMatch, + path: ['confirmNewPassword'], + }) + +export const DEFAULT_VALIDATION_SCHEMA = createValidationSchema(ZOD_MESSAGE) + +export const getChangePasswordValidationSchema = (intl: IntlFormatter) => + createValidationSchema(getZodMessages(intl)) export const DEFAULT_INITIAL_VALUES: ChangePasswordForm = { currentPassword: '', diff --git a/packages/authentication/modules/access/useLogin/constants.ts b/packages/authentication/modules/access/useLogin/constants.ts index 739976b39..3aeaaea70 100644 --- a/packages/authentication/modules/access/useLogin/constants.ts +++ b/packages/authentication/modules/access/useLogin/constants.ts @@ -1,13 +1,24 @@ -import { ZOD_MESSAGE } from '@baseapp-frontend/utils' +import { + type IntlFormatter, + ZOD_MESSAGE, + type ZodMessages, + getZodMessages, +} from '@baseapp-frontend/utils' import { z } from 'zod' import type { LoginRequest } from '../../../types/auth' -export const DEFAULT_VALIDATION_SCHEMA = z.object({ - email: z.string().min(1, ZOD_MESSAGE.required).email(ZOD_MESSAGE.email), - password: z.string().min(1, ZOD_MESSAGE.required), -}) +const createValidationSchema = (messages: ZodMessages) => + z.object({ + email: z.string().min(1, messages.required).email(messages.email), + password: z.string().min(1, messages.required), + }) + +export const DEFAULT_VALIDATION_SCHEMA = createValidationSchema(ZOD_MESSAGE) + +export const getLoginValidationSchema = (intl: IntlFormatter) => + createValidationSchema(getZodMessages(intl)) export const DEFAULT_INITIAL_VALUES: LoginRequest = { email: '', diff --git a/packages/authentication/modules/access/useLogin/index.ts b/packages/authentication/modules/access/useLogin/index.ts index 352fe9b57..5182d4f44 100644 --- a/packages/authentication/modules/access/useLogin/index.ts +++ b/packages/authentication/modules/access/useLogin/index.ts @@ -22,6 +22,7 @@ import type { LoginJWTResponse, LoginMfaRequest, } from '../../../types/auth' +import type { MfaRequest } from '../../../types/mfa' import { User } from '../../../types/user' import { isLoginChangeExpiredPasswordRedirectResponse, @@ -35,6 +36,7 @@ import type { ApiClass, LoginParams, UseLoginOptions } from './types' const useLogin = ({ loginFormOptions = {}, + mfaFormOptions = {}, loginOptions = {}, mfaOptions = {}, accessKeyName = ACCESS_KEY_NAME, @@ -114,10 +116,11 @@ const useLogin = ({ }, }) - const mfaForm = useForm({ + const mfaForm = useForm>({ defaultValues: CODE_VALIDATION_INITIAL_VALUES, resolver: zodResolver(CODE_VALIDATION_SCHEMA), mode: 'onBlur', + ...mfaFormOptions, }) const mfaMutation = useMutation({ diff --git a/packages/authentication/modules/access/useLogin/types.ts b/packages/authentication/modules/access/useLogin/types.ts index b1d3193b1..e1d77b429 100644 --- a/packages/authentication/modules/access/useLogin/types.ts +++ b/packages/authentication/modules/access/useLogin/types.ts @@ -3,6 +3,7 @@ import type { UseFormProps } from 'react-hook-form' import AuthApi from '../../../services/auth' import type { CustomJWTKeyNames, LoginMfaRequest, LoginResponse } from '../../../types/auth' +import type { MfaRequest } from '../../../types/mfa' export type ApiClass = { login: (...args: any[]) => Promise @@ -11,9 +12,11 @@ export type ApiClass = { export type LoginParams = Parameters[0] export type LoginReturn = Awaited> -export interface UseLoginOptions - extends CustomJWTKeyNames { +export interface UseLoginOptions< + TApiClass extends ApiClass = typeof AuthApi, +> extends CustomJWTKeyNames { loginFormOptions?: UseFormProps> + mfaFormOptions?: UseFormProps> loginOptions?: UseMutationOptions, unknown, LoginParams, any> mfaOptions?: UseMutationOptions ApiClass?: TApiClass diff --git a/packages/authentication/modules/access/useRecoverPassword/constants.ts b/packages/authentication/modules/access/useRecoverPassword/constants.ts index 9198f485d..5d794724c 100644 --- a/packages/authentication/modules/access/useRecoverPassword/constants.ts +++ b/packages/authentication/modules/access/useRecoverPassword/constants.ts @@ -1,12 +1,23 @@ -import { ZOD_MESSAGE } from '@baseapp-frontend/utils' +import { + type IntlFormatter, + ZOD_MESSAGE, + type ZodMessages, + getZodMessages, +} from '@baseapp-frontend/utils' import { z } from 'zod' import type { ForgotPasswordRequest } from '../../../types/auth' -export const DEFAULT_VALIDATION_SCHEMA = z.object({ - email: z.string().min(1, ZOD_MESSAGE.required).email(ZOD_MESSAGE.email), -}) +const createValidationSchema = (messages: ZodMessages) => + z.object({ + email: z.string().min(1, messages.required).email(messages.email), + }) + +export const DEFAULT_VALIDATION_SCHEMA = createValidationSchema(ZOD_MESSAGE) + +export const getRecoverPasswordValidationSchema = (intl: IntlFormatter) => + createValidationSchema(getZodMessages(intl)) export const DEFAULT_INITIAL_VALUES: ForgotPasswordRequest = { email: '', diff --git a/packages/authentication/modules/access/useRequestEmailChange/constants.ts b/packages/authentication/modules/access/useRequestEmailChange/constants.ts index 560d2b1e1..eefbdf23a 100644 --- a/packages/authentication/modules/access/useRequestEmailChange/constants.ts +++ b/packages/authentication/modules/access/useRequestEmailChange/constants.ts @@ -1,12 +1,23 @@ -import { ZOD_MESSAGE } from '@baseapp-frontend/utils' +import { + type IntlFormatter, + ZOD_MESSAGE, + type ZodMessages, + getZodMessages, +} from '@baseapp-frontend/utils' import { z } from 'zod' import type { RequestEmailChangeRequest } from './types' -export const DEFAULT_VALIDATION_SCHEMA = z.object({ - newEmail: z.string().email(ZOD_MESSAGE.email).min(1, ZOD_MESSAGE.required), -}) +const createValidationSchema = (messages: ZodMessages) => + z.object({ + newEmail: z.string().email(messages.email).min(1, messages.required), + }) + +export const DEFAULT_VALIDATION_SCHEMA = createValidationSchema(ZOD_MESSAGE) + +export const getRequestEmailChangeValidationSchema = (intl: IntlFormatter) => + createValidationSchema(getZodMessages(intl)) export const DEFAULT_INITIAL_VALUES: RequestEmailChangeRequest = { newEmail: '', diff --git a/packages/authentication/modules/access/useResetPassword/constants.ts b/packages/authentication/modules/access/useResetPassword/constants.ts index bbff43c56..5b39b1462 100644 --- a/packages/authentication/modules/access/useResetPassword/constants.ts +++ b/packages/authentication/modules/access/useResetPassword/constants.ts @@ -1,20 +1,32 @@ -import { PASSWORD_REGEX, ZOD_MESSAGE } from '@baseapp-frontend/utils' +import { + type IntlFormatter, + PASSWORD_REGEX, + ZOD_MESSAGE, + type ZodMessages, + getZodMessages, +} from '@baseapp-frontend/utils' import { z } from 'zod' import type { ResetPasswordForm } from './types' -export const DEFAULT_VALIDATION_SCHEMA = z - .object({ - newPassword: z.string().min(1, ZOD_MESSAGE.required).regex(PASSWORD_REGEX, { - message: ZOD_MESSAGE.password, - }), - confirmNewPassword: z.string().nonempty(ZOD_MESSAGE.required), - }) - .refine(({ confirmNewPassword, newPassword }) => newPassword === confirmNewPassword, { - message: ZOD_MESSAGE.passwordDoNotMatch, - path: ['confirmNewPassword'], - }) +const createValidationSchema = (messages: ZodMessages) => + z + .object({ + newPassword: z.string().min(1, messages.required).regex(PASSWORD_REGEX, { + message: messages.password, + }), + confirmNewPassword: z.string().nonempty(messages.required), + }) + .refine(({ confirmNewPassword, newPassword }) => newPassword === confirmNewPassword, { + message: messages.passwordDoNotMatch, + path: ['confirmNewPassword'], + }) + +export const DEFAULT_VALIDATION_SCHEMA = createValidationSchema(ZOD_MESSAGE) + +export const getResetPasswordValidationSchema = (intl: IntlFormatter) => + createValidationSchema(getZodMessages(intl)) export const DEFAULT_INITIAL_VALUES: ResetPasswordForm = { newPassword: '', diff --git a/packages/authentication/modules/access/useSignUp/constants.ts b/packages/authentication/modules/access/useSignUp/constants.ts index 62cf67d80..dd0af2be0 100644 --- a/packages/authentication/modules/access/useSignUp/constants.ts +++ b/packages/authentication/modules/access/useSignUp/constants.ts @@ -1,25 +1,47 @@ -import { PASSWORD_REGEX, ZOD_MESSAGE } from '@baseapp-frontend/utils' +import { + type IntlFormatter, + PASSWORD_REGEX, + ZOD_MESSAGE, + type ZodMessages, + getZodMessages, +} from '@baseapp-frontend/utils' import { z } from 'zod' import type { RegisterRequest } from '../../../types/auth' -export const DEFAULT_VALIDATION_SCHEMA_WITH_NAME = z.object({ - name: z.string().min(1, ZOD_MESSAGE.required), - password: z.string().min(1, ZOD_MESSAGE.required).regex(PASSWORD_REGEX, { - message: ZOD_MESSAGE.password, - }), - email: z.string().min(1, ZOD_MESSAGE.required).email(ZOD_MESSAGE.email), -}) - -export const DEFAULT_VALIDATION_SCHEMA = z.object({ - firstName: z.string().min(1, ZOD_MESSAGE.required), - lastName: z.string().min(1, ZOD_MESSAGE.required), - password: z.string().min(1, ZOD_MESSAGE.required).regex(PASSWORD_REGEX, { - message: ZOD_MESSAGE.password, - }), - email: z.string().min(1, ZOD_MESSAGE.required).email(ZOD_MESSAGE.email), -}) +const createPasswordSchema = (messages: ZodMessages) => + z.string().min(1, messages.required).regex(PASSWORD_REGEX, { + message: messages.password, + }) + +const createEmailSchema = (messages: ZodMessages) => + z.string().min(1, messages.required).email(messages.email) + +const createValidationSchemaWithName = (messages: ZodMessages) => + z.object({ + name: z.string().min(1, messages.required), + password: createPasswordSchema(messages), + email: createEmailSchema(messages), + }) + +const createValidationSchema = (messages: ZodMessages) => + z.object({ + firstName: z.string().min(1, messages.required), + lastName: z.string().min(1, messages.required), + password: createPasswordSchema(messages), + email: createEmailSchema(messages), + }) + +export const DEFAULT_VALIDATION_SCHEMA_WITH_NAME = createValidationSchemaWithName(ZOD_MESSAGE) + +export const DEFAULT_VALIDATION_SCHEMA = createValidationSchema(ZOD_MESSAGE) + +export const getSignUpValidationSchemaWithName = (intl: IntlFormatter) => + createValidationSchemaWithName(getZodMessages(intl)) + +export const getSignUpValidationSchema = (intl: IntlFormatter) => + createValidationSchema(getZodMessages(intl)) export const DEFAULT_INITIAL_VALUES: RegisterRequest = { name: '', diff --git a/packages/authentication/modules/mfa/constants.ts b/packages/authentication/modules/mfa/constants.ts index 299335b9f..4507d93a2 100644 --- a/packages/authentication/modules/mfa/constants.ts +++ b/packages/authentication/modules/mfa/constants.ts @@ -1,10 +1,21 @@ -import { ZOD_MESSAGE } from '@baseapp-frontend/utils' +import { + type IntlFormatter, + ZOD_MESSAGE, + type ZodMessages, + getZodMessages, +} from '@baseapp-frontend/utils' import { z } from 'zod' -export const CODE_VALIDATION_SCHEMA = z.object({ - code: z.string().min(1, ZOD_MESSAGE.required), -}) +const createCodeValidationSchema = (messages: ZodMessages) => + z.object({ + code: z.string().min(1, messages.required), + }) + +export const CODE_VALIDATION_SCHEMA = createCodeValidationSchema(ZOD_MESSAGE) + +export const getCodeValidationSchema = (intl: IntlFormatter) => + createCodeValidationSchema(getZodMessages(intl)) export const CODE_VALIDATION_INITIAL_VALUES = { code: '', diff --git a/packages/components/locales/en.json b/packages/components/locales/en.json index e43d9079d..40ad73523 100644 --- a/packages/components/locales/en.json +++ b/packages/components/locales/en.json @@ -1,12 +1,34 @@ { + "comments.actions.delete": "Delete Comment", "comments.actions.edit": "Edit Comment", "comments.actions.pin": "Pin Comment", "comments.actions.share": "Share Comment", "comments.actions.unpin": "Unpin Comment", "comments.create.placeholder": "Comment...", + "comments.create.submit.ariaLabel": "create comment", + "comments.item.actionsOverlay.title": "Comment", + "comments.item.avatar.alt": "Comment's user avatar", + "comments.item.deletedUser": "Deleted User", "comments.loading.more": "loading more comments", "comments.pinned.badge": "Pinned", + "comments.reaction.ariaLabel": "react to comment {commentId}", + "comments.reaction.count.ariaLabel": "reactions count {commentId}", "comments.replies.showMore": "Show more replies ({count})", "comments.reply.ariaLabel": "reply to comment {commentId}", - "comments.reply.count.ariaLabel": "replies count {commentId}" + "comments.reply.count.ariaLabel": "replies count {commentId}", + "comments.upsertActions.attachFiles.ariaLabel": "Attach files", + "comments.upsertActions.mention.ariaLabel": "mention", + "shared.actionsOverlay.ariaLabel": "actions overlay", + "shared.actionsOverlay.defaultTitle": "Item", + "shared.actionsOverlay.deleteDialog.content": "Are you sure you want to delete? This action cannot be undone.", + "shared.actionsOverlay.deleteDialog.title": "Delete {title}?", + "shared.actionsOverlay.deleteItem.ariaLabel": "delete item", + "shared.actionsOverlay.showMenuOptions.ariaLabel": "Show menu options", + "shared.searchNotFound.message": "Check your spelling or try another search.", + "shared.searchNotFound.title": "No results found", + "shared.socialInput.placeholder": "Message...", + "shared.socialInput.submit.ariaLabel": "submit actions", + "shared.timestamp.today": "Today", + "shared.updateSubmitActions.cancel.ariaLabel": "cancel comment edit", + "shared.updateSubmitActions.save.ariaLabel": "save comment edit" } diff --git a/packages/components/locales/es.json b/packages/components/locales/es.json index e0b5e2d35..2d69ddf9c 100644 --- a/packages/components/locales/es.json +++ b/packages/components/locales/es.json @@ -1,12 +1,34 @@ { - "comments.create.placeholder": "Comentar...", - "comments.actions.share": "Compartir Comentario", + "comments.actions.delete": "Eliminar comentario", + "comments.actions.edit": "Editar Comentario", "comments.actions.pin": "Fijar Comentario", + "comments.actions.share": "Compartir Comentario", "comments.actions.unpin": "Desfijar Comentario", - "comments.actions.edit": "Editar Comentario", + "comments.create.placeholder": "Comentar...", + "comments.create.submit.ariaLabel": "crear comentario", + "comments.item.actionsOverlay.title": "comentario", + "comments.item.avatar.alt": "Avatar del autor del comentario", + "comments.item.deletedUser": "Usuario eliminado", "comments.loading.more": "cargando más comentarios", + "comments.pinned.badge": "Fijado", + "comments.reaction.ariaLabel": "reaccionar al comentario {commentId}", + "comments.reaction.count.ariaLabel": "cantidad de reacciones {commentId}", + "comments.replies.showMore": "Mostrar más respuestas ({count})", "comments.reply.ariaLabel": "responder al comentario {commentId}", "comments.reply.count.ariaLabel": "cantidad de respuestas {commentId}", - "comments.replies.showMore": "Mostrar más respuestas ({count})", - "comments.pinned.badge": "Fijado" + "comments.upsertActions.attachFiles.ariaLabel": "Adjuntar archivos", + "comments.upsertActions.mention.ariaLabel": "mención", + "shared.actionsOverlay.ariaLabel": "menú de acciones", + "shared.actionsOverlay.defaultTitle": "elemento", + "shared.actionsOverlay.deleteDialog.content": "¿Seguro que quieres eliminarlo? Esta acción no se puede deshacer.", + "shared.actionsOverlay.deleteDialog.title": "¿Eliminar {title}?", + "shared.actionsOverlay.deleteItem.ariaLabel": "eliminar elemento", + "shared.actionsOverlay.showMenuOptions.ariaLabel": "Mostrar opciones del menú", + "shared.searchNotFound.message": "Revisa la ortografía o intenta otra búsqueda.", + "shared.searchNotFound.title": "No se encontraron resultados", + "shared.socialInput.placeholder": "Mensaje...", + "shared.socialInput.submit.ariaLabel": "enviar", + "shared.timestamp.today": "Hoy", + "shared.updateSubmitActions.cancel.ariaLabel": "cancelar edición del comentario", + "shared.updateSubmitActions.save.ariaLabel": "guardar edición del comentario" } diff --git a/packages/components/locales/pt.json b/packages/components/locales/pt.json index b6c1c5bca..993aa1836 100644 --- a/packages/components/locales/pt.json +++ b/packages/components/locales/pt.json @@ -1,12 +1,34 @@ { - "comments.create.placeholder": "Comentar...", - "comments.actions.share": "Compartilhar Comentário", + "comments.actions.delete": "Excluir comentário", + "comments.actions.edit": "Editar Comentário", "comments.actions.pin": "Fixar Comentário", + "comments.actions.share": "Compartilhar Comentário", "comments.actions.unpin": "Desafixar Comentário", - "comments.actions.edit": "Editar Comentário", + "comments.create.placeholder": "Comentar...", + "comments.create.submit.ariaLabel": "criar comentário", + "comments.item.actionsOverlay.title": "comentário", + "comments.item.avatar.alt": "Avatar do autor do comentário", + "comments.item.deletedUser": "Usuário excluído", "comments.loading.more": "carregando mais comentários", + "comments.pinned.badge": "Fixado", + "comments.reaction.ariaLabel": "reagir ao comentário {commentId}", + "comments.reaction.count.ariaLabel": "número de reações {commentId}", + "comments.replies.showMore": "Mostrar mais respostas ({count})", "comments.reply.ariaLabel": "responder ao comentário {commentId}", "comments.reply.count.ariaLabel": "contagem de respostas {commentId}", - "comments.replies.showMore": "Mostrar mais respostas ({count})", - "comments.pinned.badge": "Fixado" + "comments.upsertActions.attachFiles.ariaLabel": "Anexar arquivos", + "comments.upsertActions.mention.ariaLabel": "menção", + "shared.actionsOverlay.ariaLabel": "menu de ações", + "shared.actionsOverlay.defaultTitle": "item", + "shared.actionsOverlay.deleteDialog.content": "Tem certeza de que deseja excluir? Esta ação não pode ser desfeita.", + "shared.actionsOverlay.deleteDialog.title": "Excluir {title}?", + "shared.actionsOverlay.deleteItem.ariaLabel": "excluir item", + "shared.actionsOverlay.showMenuOptions.ariaLabel": "Mostrar opções do menu", + "shared.searchNotFound.message": "Verifique a ortografia ou tente outra pesquisa.", + "shared.searchNotFound.title": "Nenhum resultado encontrado", + "shared.socialInput.placeholder": "Mensagem...", + "shared.socialInput.submit.ariaLabel": "enviar", + "shared.timestamp.today": "Hoje", + "shared.updateSubmitActions.cancel.ariaLabel": "cancelar edição do comentário", + "shared.updateSubmitActions.save.ariaLabel": "salvar edição do comentário" } diff --git a/packages/components/modules/__shared__/web/ActionsOverlay/DefaultHoverOverlay/index.tsx b/packages/components/modules/__shared__/web/ActionsOverlay/DefaultHoverOverlay/index.tsx index 37838c867..0c4cf6f78 100644 --- a/packages/components/modules/__shared__/web/ActionsOverlay/DefaultHoverOverlay/index.tsx +++ b/packages/components/modules/__shared__/web/ActionsOverlay/DefaultHoverOverlay/index.tsx @@ -3,6 +3,8 @@ import React, { FC } from 'react' import { IconButton } from '@baseapp-frontend/design-system/components/web/buttons' import { TrashCanIcon } from '@baseapp-frontend/design-system/components/web/icons' +import { useIntl } from 'react-intl' + import { ActionOverlayTooltipContainer } from '../styled' import { DefaultHoverOverlayProps } from './types' @@ -15,38 +17,48 @@ const DefaultHoverOverlay: FC = ({ handleDeleteDialogOpen, actions = [], handleLongPressItemOptionsClose, -}) => ( - - {showDeleteButton && ( - - - - )} - {actions?.map(({ label, icon, onClick, disabled, hasPermission, closeOnClick }) => { - if (!hasPermission) return null - - const handleClick = () => { - onClick?.() - if (closeOnClick) { - handleLongPressItemOptionsClose() - } - } +}) => { + const intl = useIntl() - return ( - - {icon} + return ( + + {showDeleteButton && ( + + - ) - })} - -) + )} + {actions?.map(({ label, icon, onClick, disabled, hasPermission, closeOnClick }) => { + if (!hasPermission) return null + + const handleClick = () => { + onClick?.() + if (closeOnClick) { + handleLongPressItemOptionsClose() + } + } + + return ( + + {icon} + + ) + })} + + ) +} export default DefaultHoverOverlay diff --git a/packages/components/modules/__shared__/web/ActionsOverlay/ThreeDotsMenuHoverOverlay/index.tsx b/packages/components/modules/__shared__/web/ActionsOverlay/ThreeDotsMenuHoverOverlay/index.tsx index afedc8c62..f5a1bbdce 100644 --- a/packages/components/modules/__shared__/web/ActionsOverlay/ThreeDotsMenuHoverOverlay/index.tsx +++ b/packages/components/modules/__shared__/web/ActionsOverlay/ThreeDotsMenuHoverOverlay/index.tsx @@ -5,6 +5,7 @@ import { ThreeDotsIcon, TrashCanIcon } from '@baseapp-frontend/design-system/com import { Popover } from '@baseapp-frontend/design-system/components/web/popovers' import { MenuItem, MenuList, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { TooltipContainer } from './styled' import { ThreeDotsMenuHoverOverlayProps } from './types' @@ -19,50 +20,67 @@ const ThreeDotsMenuHoverOverlay: FC = ({ actions = [], handleClosePopover, popover, -}) => ( - - - - - - - {actions?.map(({ label, icon, onClick, disabled, hasPermission }) => { - if (!hasPermission) return null +}) => { + const intl = useIntl() - return ( + return ( + + + + + + + {actions?.map(({ label, icon, onClick, disabled, hasPermission }) => { + if (!hasPermission) return null + + return ( + { + onClick?.() + handleClosePopover() + }} + disabled={disabled} + > + {icon} + + {label} + + + ) + })} + {showDeleteButton && ( { - onClick?.() - handleClosePopover() - }} - disabled={disabled} + onClick={handleDeleteDialogOpen} + disabled={isDeletingItem || disableDeleteButton} + color="disabled" > - {icon} - - {label} + + + {intl.formatMessage({ id: 'common.delete', defaultMessage: 'Delete' })} - ) - })} - {showDeleteButton && ( - - - - Delete - - - )} - - - -) + )} + + + + ) +} export default ThreeDotsMenuHoverOverlay diff --git a/packages/components/modules/__shared__/web/ActionsOverlay/index.tsx b/packages/components/modules/__shared__/web/ActionsOverlay/index.tsx index 3c55adca9..80212f8f1 100644 --- a/packages/components/modules/__shared__/web/ActionsOverlay/index.tsx +++ b/packages/components/modules/__shared__/web/ActionsOverlay/index.tsx @@ -8,6 +8,7 @@ import { usePopover } from '@baseapp-frontend/design-system/hooks/common' import { LoadingButton } from '@mui/lab' import { Box, Divider, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { LongPressCallbackReason, useLongPress } from 'use-long-press' import DefaultHoverOverlay from './DefaultHoverOverlay' @@ -21,7 +22,7 @@ const ActionsOverlay = forwardRef { + const intl = useIntl() const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [isHoveringItem, setIsHoveringItem] = useState(false) const [longPressHandler, setLongPressHandler] = useState({ @@ -109,8 +111,21 @@ const ActionsOverlay = forwardRef ( - Delete + {intl.formatMessage({ id: 'common.delete', defaultMessage: 'Delete' })} } onClose={handleDeleteDialogClose} @@ -139,7 +154,10 @@ const ActionsOverlay = forwardRef @@ -179,7 +197,10 @@ const ActionsOverlay = forwardRef @@ -191,7 +212,7 @@ const ActionsOverlay = forwardRef - Delete + {intl.formatMessage({ id: 'common.delete', defaultMessage: 'Delete' })} diff --git a/packages/components/modules/__shared__/web/SearchNotFoundState/index.tsx b/packages/components/modules/__shared__/web/SearchNotFoundState/index.tsx index 9fcd83468..747c939cf 100644 --- a/packages/components/modules/__shared__/web/SearchNotFoundState/index.tsx +++ b/packages/components/modules/__shared__/web/SearchNotFoundState/index.tsx @@ -1,20 +1,24 @@ import { SearchingImage } from '@baseapp-frontend/design-system/components/web/illustrations' import { Box, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' import { SearchNotFoundStateProps } from '../../common/types' -const SearchNotFoundState = ({ - message = 'Check your spelling or try another search.', -}: SearchNotFoundStateProps) => ( +const SearchNotFoundState = ({ message }: SearchNotFoundStateProps) => ( - No results found + - {message} + {message ?? ( + + )} diff --git a/packages/components/modules/__shared__/web/SocialInput/SubmitActions/index.tsx b/packages/components/modules/__shared__/web/SocialInput/SubmitActions/index.tsx index c326782d4..f0ddb4a28 100644 --- a/packages/components/modules/__shared__/web/SocialInput/SubmitActions/index.tsx +++ b/packages/components/modules/__shared__/web/SocialInput/SubmitActions/index.tsx @@ -3,18 +3,35 @@ import React, { FC } from 'react' import { IconButton } from '@baseapp-frontend/design-system/components/web/buttons' import { SendMessageIcon as DefaultSendMessageIcon } from '@baseapp-frontend/design-system/components/web/icons' +import { useIntl } from 'react-intl' + import { SubmitActionsProps } from './types' const SubmitActions: FC = ({ formId, disabled = false, - ariaLabel = 'submit actions', + ariaLabel, SendMessageIcon = DefaultSendMessageIcon, SendMessageIconProps = {}, -}) => ( - - - -) +}) => { + const intl = useIntl() + + return ( + + + + ) +} export default SubmitActions diff --git a/packages/components/modules/__shared__/web/SocialInput/index.tsx b/packages/components/modules/__shared__/web/SocialInput/index.tsx index ed02da326..481252514 100644 --- a/packages/components/modules/__shared__/web/SocialInput/index.tsx +++ b/packages/components/modules/__shared__/web/SocialInput/index.tsx @@ -4,6 +4,8 @@ import { KeyboardEvent, KeyboardEventHandler, forwardRef } from 'react' import { SocialTextField as DefaultSocialTextField } from '@baseapp-frontend/design-system/components/web/inputs' +import { useIntl } from 'react-intl' + import { SOCIAL_UPSERT_FORM } from '../../common' import DefaultSocialUpsertActions from '../SocialUpsertActions' import DefaultSubmitActions from './SubmitActions' @@ -61,7 +63,7 @@ import { SocialInputProps } from './types' const SocialInput = forwardRef( ( { - placeholder = 'Message...', + placeholder, autoFocusInput, mode, SocialTextField = DefaultSocialTextField, @@ -84,6 +86,7 @@ const SocialInput = forwardRef( }, ref, ) => { + const intl = useIntl() const defaultKeyDown = (event: KeyboardEvent, onSubmit: VoidFunction) => { if (event.key === 'Enter' && event.ctrlKey) { event.preventDefault() @@ -111,7 +114,13 @@ const SocialInput = forwardRef( name={SOCIAL_UPSERT_FORM.body} control={form.control} onKeyDown={handleKeyDown} - placeholder={placeholder} + placeholder={ + placeholder ?? + intl.formatMessage({ + id: 'shared.socialInput.placeholder', + defaultMessage: 'Message...', + }) + } autoFocus={autoFocusInput} mode={mode} isReply={isReply} diff --git a/packages/components/modules/__shared__/web/Timestamp/index.tsx b/packages/components/modules/__shared__/web/Timestamp/index.tsx index cb4ee264a..355901576 100644 --- a/packages/components/modules/__shared__/web/Timestamp/index.tsx +++ b/packages/components/modules/__shared__/web/Timestamp/index.tsx @@ -4,6 +4,7 @@ import { DATE_FORMAT, TIME_FORMAT, formatDate } from '@baseapp-frontend/utils' import { Typography } from '@mui/material' import { DateTime } from 'luxon' +import { FormattedMessage } from 'react-intl' import { Dot } from './styled' import { TimestampProps } from './types' @@ -14,7 +15,11 @@ const Timestamp: FC = ({ date }) => { return ( - {isToday ? 'Today' : formatDate(dateTime, { toFormat: TIME_FORMAT[3] })} + {isToday ? ( + + ) : ( + formatDate(dateTime, { toFormat: TIME_FORMAT[3] }) + )} {formatDate(dateTime, { toFormat: DATE_FORMAT[2] })} diff --git a/packages/components/modules/__shared__/web/UpdateSubmitActions/index.tsx b/packages/components/modules/__shared__/web/UpdateSubmitActions/index.tsx index d78798915..a7dbfedfa 100644 --- a/packages/components/modules/__shared__/web/UpdateSubmitActions/index.tsx +++ b/packages/components/modules/__shared__/web/UpdateSubmitActions/index.tsx @@ -3,24 +3,50 @@ import React, { FC } from 'react' import { IconButton } from '@baseapp-frontend/design-system/components/web/buttons' import { CheckMarkIcon, CloseIcon } from '@baseapp-frontend/design-system/components/web/icons' +import { useIntl } from 'react-intl' + import { ActionsContainer } from './styled' import { UpdateSubmitActionsProps } from './types' const UpdateSubmitActions: FC = ({ formId, disabled = false, - ariaLabel = 'save comment edit', - cancelAriaLabel = 'cancel comment edit', + ariaLabel, + cancelAriaLabel, handleEditCancel = () => {}, -}) => ( - - - - - - - - -) +}) => { + const intl = useIntl() + + return ( + + + + + + + + + ) +} export default UpdateSubmitActions diff --git a/packages/components/modules/comments/common/hooks/useCommentActions/index.ts b/packages/components/modules/comments/common/hooks/useCommentActions/index.ts index f694fdbec..67e9a4f0e 100644 --- a/packages/components/modules/comments/common/hooks/useCommentActions/index.ts +++ b/packages/components/modules/comments/common/hooks/useCommentActions/index.ts @@ -1,5 +1,3 @@ -import { useIntl } from 'react-intl' - import { useCommentPinMutation } from '../../graphql/mutations/CommentPin' import { CommentAction, UseCommentActionsOptions } from './types' @@ -16,7 +14,6 @@ const useCommentActions = ({ enableShare = true, shareDisabled = true, }: UseCommentActionsOptions): CommentAction[] => { - const intl = useIntl() const [pinComment, isPinningComment] = useCommentPinMutation() const handlePinComment = () => { @@ -29,22 +26,10 @@ const useCommentActions = ({ }) } - const pinMessage = intl.formatMessage({ - id: 'comments.actions.pin', - defaultMessage: 'Pin Comment', - }) - const unpinMessage = intl.formatMessage({ - id: 'comments.actions.unpin', - defaultMessage: 'Unpin Comment', - }) - return [ { id: 'share', - label: intl.formatMessage({ - id: 'comments.actions.share', - defaultMessage: 'Share Comment', - }), + label: 'Share Comment', hasPermission: enableShare, disabled: shareDisabled, closeOnSelect: true, @@ -52,7 +37,7 @@ const useCommentActions = ({ }, { id: 'pin', - label: comment?.isPinned ? unpinMessage : pinMessage, + label: `${comment?.isPinned ? 'Unpin' : 'Pin'} Comment`, hasPermission: comment?.canPin, disabled: isPinningComment, closeOnSelect: true, @@ -60,10 +45,7 @@ const useCommentActions = ({ }, { id: 'edit', - label: intl.formatMessage({ - id: 'comments.actions.edit', - defaultMessage: 'Edit Comment', - }), + label: 'Edit Comment', hasPermission: comment?.canChange, disabled: false, closeOnSelect: true, diff --git a/packages/components/modules/comments/web/CommentCreate/CommentFilesUpsertActions/index.tsx b/packages/components/modules/comments/web/CommentCreate/CommentFilesUpsertActions/index.tsx index 9c1f0c430..77da3ffc1 100644 --- a/packages/components/modules/comments/web/CommentCreate/CommentFilesUpsertActions/index.tsx +++ b/packages/components/modules/comments/web/CommentCreate/CommentFilesUpsertActions/index.tsx @@ -6,6 +6,8 @@ import { useFileSelect } from '@baseapp-frontend/components/files/common' import { IconButton } from '@baseapp-frontend/design-system/components/web/buttons' import { AttachmentIcon, MentionIcon } from '@baseapp-frontend/design-system/components/web/icons' +import { useIntl } from 'react-intl' + import { ActionsContainer } from './styled' import type { CommentFilesUpsertActionsProps } from './types' @@ -21,6 +23,7 @@ const CommentFilesUpsertActions: FC = ({ maxFileSize, acceptedFileTypes, }) => { + const intl = useIntl() const { open, getInputProps } = useFileSelect({ onFilesSelected, maxFiles, @@ -38,11 +41,20 @@ const CommentFilesUpsertActions: FC = ({ onClick={open} disabled={isUploading} isLoading={isUploading} - aria-label="Attach files" + aria-label={intl.formatMessage({ + id: 'comments.upsertActions.attachFiles.ariaLabel', + defaultMessage: 'Attach files', + })} > - + diff --git a/packages/components/modules/comments/web/CommentCreate/index.tsx b/packages/components/modules/comments/web/CommentCreate/index.tsx index 34323177b..acf4a7c27 100644 --- a/packages/components/modules/comments/web/CommentCreate/index.tsx +++ b/packages/components/modules/comments/web/CommentCreate/index.tsx @@ -146,7 +146,10 @@ const CommentCreate = forwardRef( replyTargetName={replyTargetName} onCancelReply={cancelReply} SubmitActionsProps={{ - ariaLabel: 'create comment', + ariaLabel: intl.formatMessage({ + id: 'comments.create.submit.ariaLabel', + defaultMessage: 'create comment', + }), }} {...mergedSocialInputProps} SocialUpsertActions={CommentFilesUpsertActions} diff --git a/packages/components/modules/comments/web/CommentItem/CommentReactionButton/index.tsx b/packages/components/modules/comments/web/CommentItem/CommentReactionButton/index.tsx index 484a60bea..80a38548c 100644 --- a/packages/components/modules/comments/web/CommentItem/CommentReactionButton/index.tsx +++ b/packages/components/modules/comments/web/CommentItem/CommentReactionButton/index.tsx @@ -7,32 +7,49 @@ import { } from '@baseapp-frontend/design-system/components/web/icons' import { Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { ReactionButton } from '../../../../__shared__/common' import { CounterContainer } from './styled' import { CommentReactionButtonProps } from './types' -const CommentReactionButton: FC = ({ target: targetRef, onClick }) => ( - - {({ handleReaction, target }) => ( - - - {target?.myReaction?.id ? ( - - ) : ( - - )} - - - {target?.reactionsCount?.total} - - - )} - -) +const CommentReactionButton: FC = ({ target: targetRef, onClick }) => { + const intl = useIntl() + + return ( + + {({ handleReaction, target }) => ( + + + {target?.myReaction?.id ? ( + + ) : ( + + )} + + + {target?.reactionsCount?.total} + + + )} + + ) +} export default CommentReactionButton diff --git a/packages/components/modules/comments/web/CommentItem/index.tsx b/packages/components/modules/comments/web/CommentItem/index.tsx index 6993a727d..c8b291ca7 100644 --- a/packages/components/modules/comments/web/CommentItem/index.tsx +++ b/packages/components/modules/comments/web/CommentItem/index.tsx @@ -8,6 +8,7 @@ import { Markdown } from '@baseapp-frontend/design-system/components/web/markdow import { Typography } from '@mui/material' import Link from 'next/link' import { useRouter } from 'next/navigation' +import { useIntl } from 'react-intl' import { ActionsOverlay, Timestamp as DefaultTimestamp } from '../../../__shared__/web' import { FileUploadList } from '../../../files/web' @@ -66,6 +67,7 @@ const CommentItem: FC = ({ profileUrl, } = useCommentItem({ comment: commentRef, useProfileId, profilePath }) const router = useRouter() + const intl = useIntl() const [isEditMode, setIsEditMode] = useState(false) @@ -86,7 +88,12 @@ const CommentItem: FC = ({ } const renderProfileName = () => { - if (!hasUser) return Deleted User + if (!hasUser) + return ( + + {intl.formatMessage({ id: 'comments.item.deletedUser', defaultMessage: 'Deleted User' })} + + ) return ( @@ -121,7 +128,10 @@ const CommentItem: FC = ({ showDeleteButton={enableDelete && comment.canDelete} handleDeleteItem={deleteComment} isDeletingItem={isDeletingComment} - title="Comment" + title={intl.formatMessage({ + id: 'comments.item.actionsOverlay.title', + defaultMessage: 'Comment', + })} {...restOfActionOverlayProps} ref={commentItemRef} > @@ -130,7 +140,13 @@ const CommentItem: FC = ({ deletedUser={!hasUser} width={40} height={40} - alt={comment.profile?.name ?? `Comment's user avatar`} + alt={ + comment.profile?.name ?? + intl.formatMessage({ + id: 'comments.item.avatar.alt', + defaultMessage: "Comment's user avatar", + }) + } src={comment.profile?.image || ''} onClick={() => router.push(profileUrl)} /> diff --git a/packages/components/modules/comments/web/CommentItem/useCommentOptions/constants.ts b/packages/components/modules/comments/web/CommentItem/useCommentOptions/constants.ts new file mode 100644 index 000000000..93223b5ae --- /dev/null +++ b/packages/components/modules/comments/web/CommentItem/useCommentOptions/constants.ts @@ -0,0 +1,24 @@ +import { defineMessages } from 'react-intl' + +export const COMMENT_ACTION_MESSAGES = defineMessages({ + share: { + id: 'comments.actions.share', + defaultMessage: 'Share Comment', + }, + pin: { + id: 'comments.actions.pin', + defaultMessage: 'Pin Comment', + }, + unpin: { + id: 'comments.actions.unpin', + defaultMessage: 'Unpin Comment', + }, + edit: { + id: 'comments.actions.edit', + defaultMessage: 'Edit Comment', + }, + delete: { + id: 'comments.actions.delete', + defaultMessage: 'Delete Comment', + }, +}) diff --git a/packages/components/modules/comments/web/CommentItem/useCommentOptions/index.tsx b/packages/components/modules/comments/web/CommentItem/useCommentOptions/index.tsx index 936b8cbab..051e87f0d 100644 --- a/packages/components/modules/comments/web/CommentItem/useCommentOptions/index.tsx +++ b/packages/components/modules/comments/web/CommentItem/useCommentOptions/index.tsx @@ -6,19 +6,23 @@ import { PinIcon, } from '@baseapp-frontend/design-system/components/web/icons' +import { useIntl } from 'react-intl' + import { OverlayAction } from '../../../../__shared__/web' import { CommentActionId, useCommentActions } from '../../../common' +import { COMMENT_ACTION_MESSAGES } from './constants' import { UseCommentOptionsParams } from './types' /** * Thin adapter mapping the shared, headless `useCommentActions` descriptors to the web - * `ActionsOverlay` action shape — only the icons are decided here. + * `ActionsOverlay` action shape — only the icons and translated labels are decided here. */ const useCommentOptions = ({ comment, onEdit, enableShare = true, }: UseCommentOptionsParams): OverlayAction[] => { + const intl = useIntl() const actions = useCommentActions({ comment, onEdit, enableShare }) const iconByActionId: Partial> = { @@ -27,10 +31,19 @@ const useCommentOptions = ({ edit: , } + const labelByActionId: Record = { + share: intl.formatMessage(COMMENT_ACTION_MESSAGES.share), + pin: intl.formatMessage( + comment?.isPinned ? COMMENT_ACTION_MESSAGES.unpin : COMMENT_ACTION_MESSAGES.pin, + ), + edit: intl.formatMessage(COMMENT_ACTION_MESSAGES.edit), + delete: intl.formatMessage(COMMENT_ACTION_MESSAGES.delete), + } + return actions.map((action) => ({ disabled: !!action.disabled, icon: iconByActionId[action.id], - label: action.label, + label: labelByActionId[action.id], onClick: action.onSelect, hasPermission: action.hasPermission, closeOnClick: action.closeOnSelect ?? true, diff --git a/packages/components/modules/comments/web/CommentUpdate/CommentFilesUpsertActions/index.tsx b/packages/components/modules/comments/web/CommentUpdate/CommentFilesUpsertActions/index.tsx index 9b8b18398..1aac72a7a 100644 --- a/packages/components/modules/comments/web/CommentUpdate/CommentFilesUpsertActions/index.tsx +++ b/packages/components/modules/comments/web/CommentUpdate/CommentFilesUpsertActions/index.tsx @@ -6,6 +6,8 @@ import { FileUploadTrigger } from '@baseapp-frontend/components/files/web' import { IconButton } from '@baseapp-frontend/design-system/components/web/buttons' import { AttachmentIcon, MentionIcon } from '@baseapp-frontend/design-system/components/web/icons' +import { useIntl } from 'react-intl' + import { ActionsContainer } from './styled' import type { CommentFilesUpsertActionsProps } from './types' @@ -15,20 +17,34 @@ const MAX_FILES = 5 * SocialInput upsert-action bar for the comment editor: the attach icon is the * real file upload trigger; the mention icon stays a placeholder until wired. */ -const CommentFilesUpsertActions: FC = ({ target }) => ( - - } - maxFiles={MAX_FILES} - autoAttach - /> - - - - -) +const CommentFilesUpsertActions: FC = ({ target }) => { + const intl = useIntl() + + return ( + + } + maxFiles={MAX_FILES} + autoAttach + label={intl.formatMessage({ + id: 'comments.upsertActions.attachFiles.ariaLabel', + defaultMessage: 'Attach files', + })} + /> + + + + + ) +} export default CommentFilesUpsertActions export type { CommentFilesUpsertActionsProps } from './types' diff --git a/packages/components/modules/messages/web/SendMessage/__tests__/SendMessage.test.tsx b/packages/components/modules/messages/web/SendMessage/__tests__/SendMessage.test.tsx index bc0af2050..6a033b444 100644 --- a/packages/components/modules/messages/web/SendMessage/__tests__/SendMessage.test.tsx +++ b/packages/components/modules/messages/web/SendMessage/__tests__/SendMessage.test.tsx @@ -1,7 +1,8 @@ -import { Suspense } from 'react' +import { PropsWithChildren, ReactElement, Suspense } from 'react' -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { act, fireEvent, render as rtlRender, screen, waitFor } from '@testing-library/react' import { useController, useFormState } from 'react-hook-form' +import { IntlProvider } from 'react-intl' import { vi } from 'vitest' import SendMessage from '../index' @@ -87,6 +88,11 @@ const renderSuspendingRoom = (roomId: string) => ( ) +const IntlWrapper = ({ children }: PropsWithChildren) => ( + {children} +) +const render = (ui: ReactElement) => rtlRender(ui, { wrapper: IntlWrapper }) + const messageInput = () => screen.getByLabelText('message') as HTMLInputElement const sendButton = () => screen.getByText('send') as HTMLButtonElement diff --git a/packages/components/package.json b/packages/components/package.json index 5ffbcacae..fbfaf72f0 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -182,7 +182,7 @@ "@baseapp-frontend/tsconfig": "workspace:*", "@chromatic-com/storybook": "catalog:storybook", "@faker-js/faker": "catalog:test", - "@formatjs/cli": "^6.7.4", + "@formatjs/cli": "catalog:", "@playwright/test": "1.62.0", "@storybook/addon-docs": "catalog:storybook", "@storybook/addon-essentials": "catalog:storybook", diff --git a/packages/design-system/.storybook/preview.ts b/packages/design-system/.storybook/preview.ts index 747e56a46..4ed04a87d 100644 --- a/packages/design-system/.storybook/preview.ts +++ b/packages/design-system/.storybook/preview.ts @@ -4,7 +4,7 @@ import type { Preview } from '@storybook/react' import '../styles/web/material/globals.css' import '../styles/web/tailwind/globals.css' -import withDesignSystemTestProviders from '../tests/web/utils/providers/withDesignSystemTestProviders' +import withDesignSystemTestProviders from '../tests/web/utils/withDesignSystemTestProviders' const preview: Preview = { decorators: [withStorybookProvidersWrapper(withDesignSystemTestProviders)], diff --git a/packages/design-system/components/web/avatars/AvatarWithPlaceholder/index.tsx b/packages/design-system/components/web/avatars/AvatarWithPlaceholder/index.tsx index 4aa48d5c7..d637f486c 100644 --- a/packages/design-system/components/web/avatars/AvatarWithPlaceholder/index.tsx +++ b/packages/design-system/components/web/avatars/AvatarWithPlaceholder/index.tsx @@ -1,5 +1,9 @@ +'use client' + import { FC } from 'react' +import { useIntl } from 'react-intl' + import { AvatarDeletedUserIcon } from '../../icons' import AvatarUploadFallbackIcon from '../../icons/AvatarUploadFallbackIcon' import { AvatarStyled } from './styled' @@ -14,22 +18,38 @@ const AvatarWithPlaceholder: FC = ({ alt, showDeletedUser = false, ...props -}) => ( - - {children || - (showDeletedUser ? ( - - ) : ( - - ))} - -) +}) => { + const intl = useIntl() + + return ( + + {children || + (showDeletedUser ? ( + + ) : ( + + ))} + + ) +} export default AvatarWithPlaceholder diff --git a/packages/design-system/components/web/avatars/ClickableAvatar/index.tsx b/packages/design-system/components/web/avatars/ClickableAvatar/index.tsx index fbf505f11..0bc768ef4 100644 --- a/packages/design-system/components/web/avatars/ClickableAvatar/index.tsx +++ b/packages/design-system/components/web/avatars/ClickableAvatar/index.tsx @@ -3,6 +3,7 @@ import { FC } from 'react' import { m } from 'framer-motion' +import { useIntl } from 'react-intl' import { varHover } from '../../animate/variants' import { AvatarDeletedUserIcon } from '../../icons' @@ -19,10 +20,18 @@ const ClickableAvatar: FC = ({ children, ...props }) => { + const intl = useIntl() + if (deletedUser) { return ( - + ) } diff --git a/packages/design-system/components/web/buttons/FileUploadButton/index.tsx b/packages/design-system/components/web/buttons/FileUploadButton/index.tsx index 45a3476ad..dc4370da5 100644 --- a/packages/design-system/components/web/buttons/FileUploadButton/index.tsx +++ b/packages/design-system/components/web/buttons/FileUploadButton/index.tsx @@ -6,10 +6,12 @@ import { useNotification } from '@baseapp-frontend/utils' import { Button, Input } from '@mui/material' import { Controller } from 'react-hook-form' +import { useIntl } from 'react-intl' import { FileUploadButtonProps } from './types' const FileUploadButton: FC = (props) => { + const intl = useIntl() const fileRef = useRef(undefined) const { sendToast } = useNotification() const { accept, maxSize, control, label, name, setFile, ...buttonProps } = props @@ -23,7 +25,11 @@ const FileUploadButton: FC = (props) => { disableRipple {...buttonProps} > - {label ?? 'Upload File'} + {label ?? + intl.formatMessage({ + id: 'designSystem.fileUploadButton.label', + defaultMessage: 'Upload File', + })} = (props) => { ) => { const { files } = event.target as HTMLInputElement if (files![0] && maxSize && files![0].size > maxSize) { - sendToast(`This file is too large (max ${maxSize / 1024 / 1024}MB).`, { - type: 'error', - }) + sendToast( + intl.formatMessage( + { + id: 'designSystem.fileUploadButton.fileTooLarge', + defaultMessage: 'This file is too large (max {maxSize}MB).', + }, + { maxSize: maxSize / 1024 / 1024 }, + ), + { type: 'error' }, + ) } else { field.onChange(files![0]) setFile(name, files![0], { diff --git a/packages/design-system/components/web/dialogs/ConfirmDialog/index.tsx b/packages/design-system/components/web/dialogs/ConfirmDialog/index.tsx index ec8457f44..7dc960e0b 100644 --- a/packages/design-system/components/web/dialogs/ConfirmDialog/index.tsx +++ b/packages/design-system/components/web/dialogs/ConfirmDialog/index.tsx @@ -6,6 +6,7 @@ import Button from '@mui/material/Button' import DialogActions from '@mui/material/DialogActions' import DialogContent from '@mui/material/DialogContent' import DialogTitle from '@mui/material/DialogTitle' +import { useIntl } from 'react-intl' import Dialog from '../Dialog' import { ConfirmDialogProps } from './types' @@ -14,45 +15,49 @@ const ConfirmDialog: FC = ({ title, content, action, - cancelText = 'Cancel', + cancelText, onClose, hideCancelButton = false, DialogTitleProps = {}, DialogContentProps = {}, DialogActionsProps = {}, ...props -}) => ( - - - {title} - - {content && ( - { + const intl = useIntl() + + return ( + + + {title} + + {content && ( + + {content} + + )} + .MuiButtonBase-root': { + width: 'fit-content', + }, }} - {...DialogContentProps} + {...DialogActionsProps} > - {content} - - )} - .MuiButtonBase-root': { - width: 'fit-content', - }, - }} - {...DialogActionsProps} - > - {!hideCancelButton && ( - - )} - {action} - - -) + {!hideCancelButton && ( + + )} + {action} + + + ) +} export default ConfirmDialog diff --git a/packages/design-system/components/web/dropzones/Dropzone/DropzonePreview/index.tsx b/packages/design-system/components/web/dropzones/Dropzone/DropzonePreview/index.tsx index 4a3bb8345..af69d5716 100644 --- a/packages/design-system/components/web/dropzones/Dropzone/DropzonePreview/index.tsx +++ b/packages/design-system/components/web/dropzones/Dropzone/DropzonePreview/index.tsx @@ -1,6 +1,7 @@ import { FC, useEffect, useState } from 'react' import { CloseRounded as CloseRoundedIcon } from '@mui/icons-material' +import { useIntl } from 'react-intl' import { FileWrapper, RemoveFileButton } from '../styled' import { DropzonePreviewProps } from '../types' @@ -11,6 +12,7 @@ const DropzonePreview: FC = ({ handleRemoveFile, onFileClick, }) => { + const intl = useIntl() const [objectUrl, setObjectUrl] = useState(undefined) useEffect(() => { @@ -36,7 +38,14 @@ const DropzonePreview: FC = ({ > {typeof = ({ { event.stopPropagation() handleRemoveFile() diff --git a/packages/design-system/components/web/dropzones/Dropzone/index.tsx b/packages/design-system/components/web/dropzones/Dropzone/index.tsx index 30783d136..32b6f8c67 100644 --- a/packages/design-system/components/web/dropzones/Dropzone/index.tsx +++ b/packages/design-system/components/web/dropzones/Dropzone/index.tsx @@ -10,6 +10,7 @@ import { } from '@mui/icons-material' import { Button, Card, Typography } from '@mui/material' import { useDropzone } from 'react-dropzone' +import { FormattedMessage, useIntl } from 'react-intl' import DefaultDropzonePreview from './DropzonePreview' import { @@ -20,10 +21,10 @@ import { ContentContainer, DropzoneContainer, DropzoneGrid, - DropzoneText, InputContainer, } from './styled' import { DropzoneProps } from './types' +import { renderBrowseText } from './utils' const Dropzone: FC = ({ accept, @@ -31,10 +32,10 @@ const Dropzone: FC = ({ onSelect, onRemove, includeActionButton = false, - actionText = 'Upload Image', + actionText, maxFileSize = 15, title, - subTitle = `Max. File Size: ${maxFileSize}MB`, + subTitle: subTitleProp, DropzoneOptions, InputProps, InputContainerStyle, @@ -48,15 +49,34 @@ const Dropzone: FC = ({ DropZoneCancelIcon = CancelIcon, DropZoneCancelIconProps = {}, }) => { + const intl = useIntl() const [files, setFiles] = useState(storedImg) const { sendToast } = useNotification() + const subTitle = + subTitleProp ?? + intl.formatMessage( + { + id: 'designSystem.dropzone.maxFileSize', + defaultMessage: 'Max. File Size: {maxFileSize}MB', + }, + { maxFileSize }, + ) const { open, getRootProps, getInputProps, isFocused, isDragAccept, isDragReject } = useDropzone({ accept, onDrop: async (acceptedFiles) => { if (acceptedFiles.length === 0 || !acceptedFiles[0]) return if ((acceptedFiles[0]?.size || 0) > maxFileSize * 1024 * 1024) { - sendToast(`This file is too large (max ${maxFileSize} MB).`, { type: 'error' }) + sendToast( + intl.formatMessage( + { + id: 'designSystem.dropzone.fileTooLarge', + defaultMessage: 'This file is too large (max {maxFileSize} MB).', + }, + { maxFileSize }, + ), + { type: 'error' }, + ) return } @@ -92,7 +112,10 @@ const Dropzone: FC = ({ const hasFiles = Boolean((!multiple && files) || (multiple && (files as [])?.length)) const renderContent = () => { - const ariaLabel = 'Drag and drop files to upload' + const ariaLabel = intl.formatMessage({ + id: 'designSystem.dropzone.inputAriaLabel', + defaultMessage: 'Drag and drop files to upload', + }) return ( @@ -106,7 +129,10 @@ const Dropzone: FC = ({ <> - File not accepted, please choose the correct type + {subTitle} @@ -125,9 +151,11 @@ const Dropzone: FC = ({ /> {title || ( - <> - Click to browse or drag and drop. - + )} @@ -183,11 +211,15 @@ const Dropzone: FC = ({ {includeActionButton && ( {files && ( )} diff --git a/packages/design-system/components/web/dropzones/Dropzone/utils.tsx b/packages/design-system/components/web/dropzones/Dropzone/utils.tsx new file mode 100644 index 000000000..23ba2086b --- /dev/null +++ b/packages/design-system/components/web/dropzones/Dropzone/utils.tsx @@ -0,0 +1,5 @@ +import { ReactNode } from 'react' + +import { DropzoneText } from './styled' + +export const renderBrowseText = (chunks: ReactNode[]) => {chunks} diff --git a/packages/design-system/components/web/inputs/AutocompleteField/index.tsx b/packages/design-system/components/web/inputs/AutocompleteField/index.tsx index 52b7ac95a..c5927d77f 100644 --- a/packages/design-system/components/web/inputs/AutocompleteField/index.tsx +++ b/packages/design-system/components/web/inputs/AutocompleteField/index.tsx @@ -10,6 +10,7 @@ import { CircularProgress, TextField, } from '@mui/material' +import { useIntl } from 'react-intl' import { AutocompleteFieldProps } from './types' @@ -24,7 +25,7 @@ import { AutocompleteFieldProps } from './types' */ const AutocompleteField: FC = ({ isPending = false, - placeholder = 'Search', + placeholder, renderInput, selectOnFocus = true, handleHomeEndKeys = true, @@ -33,36 +34,48 @@ const AutocompleteField: FC = ({ error, helperText, ...props -}) => ( - ( - - {isPending ? : null} - {params.InputProps.endAdornment} - - ), - }} - /> - )) - } - {...props} - /> -) +}) => { + const intl = useIntl() + + return ( + ( + + {isPending ? ( + + ) : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )) + } + {...props} + /> + ) +} export default withController(AutocompleteField, { shouldDebounce: true }) diff --git a/packages/design-system/components/web/inputs/MarkdownEditorField/InitializedMDXEditor/index.tsx b/packages/design-system/components/web/inputs/MarkdownEditorField/InitializedMDXEditor/index.tsx index cd4edb104..e4e60f45f 100644 --- a/packages/design-system/components/web/inputs/MarkdownEditorField/InitializedMDXEditor/index.tsx +++ b/packages/design-system/components/web/inputs/MarkdownEditorField/InitializedMDXEditor/index.tsx @@ -4,6 +4,7 @@ import { FC, useCallback, useMemo } from 'react' import { MDXEditor, + type Translation, codeBlockPlugin, codeMirrorPlugin, diffSourcePlugin, @@ -18,9 +19,10 @@ import { thematicBreakPlugin, toolbarPlugin, } from '@mdxeditor/editor' +import { useIntl } from 'react-intl' import DefaultToolbar from '../Toolbar' -import { CODE_BLOCK_LANGUAGES, DEFAULT_TOOLBAR_CONFIG } from '../constants' +import { CODE_BLOCK_LANGUAGES, DEFAULT_TOOLBAR_CONFIG, MDX_EDITOR_MESSAGES } from '../constants' import { keyboardCommandsPlugin } from './plugins/keyboard-commands' import { mentionsPlugin } from './plugins/mentions' import { InitializedMDXEditorProps } from './types' @@ -37,6 +39,20 @@ const InitializedMDXEditor: FC = ({ mentions, ...props }) => { + const intl = useIntl() + + const translation = useCallback( + (key, defaultValue, interpolations = {}) => { + const message = MDX_EDITOR_MESSAGES[key as keyof typeof MDX_EDITOR_MESSAGES] + if (message) return intl.formatMessage(message, interpolations) + return Object.entries(interpolations).reduce( + (value, [name, arg]) => value.replaceAll(`{{${name}}}`, String(arg as string | number)), + defaultValue, + ) + }, + [intl], + ) + const mergedConfig = useMemo( () => ({ ...DEFAULT_TOOLBAR_CONFIG, ...toolbarConfig }), [toolbarConfig], @@ -79,7 +95,13 @@ const InitializedMDXEditor: FC = ({ ) return ( - + ) } diff --git a/packages/design-system/components/web/inputs/MarkdownEditorField/InitializedMDXEditor/plugins/mentions/MentionsMenuPortal/index.tsx b/packages/design-system/components/web/inputs/MarkdownEditorField/InitializedMDXEditor/plugins/mentions/MentionsMenuPortal/index.tsx index 2422f7611..a6aeab8be 100644 --- a/packages/design-system/components/web/inputs/MarkdownEditorField/InitializedMDXEditor/plugins/mentions/MentionsMenuPortal/index.tsx +++ b/packages/design-system/components/web/inputs/MarkdownEditorField/InitializedMDXEditor/plugins/mentions/MentionsMenuPortal/index.tsx @@ -4,6 +4,7 @@ import { removeLeadingSlash } from '@baseapp-frontend/utils' import { useCellValue, usePublisher, useRealm } from '@mdxeditor/gurx' import { ClickAwayListener } from '@mui/material' +import { useIntl } from 'react-intl' import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso' import { @@ -23,6 +24,7 @@ import { MENTION_ROW_HEIGHT, MENTION_VISIBLE_ROWS } from './constants' import { StyledPaper, StyledPopper } from './styled' const MentionsMenuPortal: FC = () => { + const intl = useIntl() const realm = useRealm() const menuState = useCellValue(mentionMenuState$) const config = useCellValue(mentionsConfig$) @@ -146,7 +148,13 @@ const MentionsMenuPortal: FC = () => { { name: 'preventOverflow', options: { padding: 8 } }, ]} > - + {renderContent()} diff --git a/packages/design-system/components/web/inputs/MarkdownEditorField/constants.ts b/packages/design-system/components/web/inputs/MarkdownEditorField/constants.ts index 3b31ca968..848c93020 100644 --- a/packages/design-system/components/web/inputs/MarkdownEditorField/constants.ts +++ b/packages/design-system/components/web/inputs/MarkdownEditorField/constants.ts @@ -1,3 +1,5 @@ +import { defineMessages } from 'react-intl' + import { ToolbarConfig } from './types' export const DEFAULT_TOOLBAR_CONFIG: Required = { @@ -31,3 +33,255 @@ export const CODE_BLOCK_LANGUAGES: Record = { tsx: 'TypeScript (React)', jsx: 'JavaScript (React)', } + +// Keyed by MDXEditor translation key +export const MDX_EDITOR_MESSAGES = defineMessages({ + 'codeBlock.inlineLanguage': { + id: 'designSystem.markdownEditor.codeBlock.inlineLanguage', + defaultMessage: 'Language', + }, + 'codeBlock.language': { + id: 'designSystem.markdownEditor.codeBlock.language', + defaultMessage: 'Code block language', + }, + 'codeBlock.selectLanguage': { + id: 'designSystem.markdownEditor.codeBlock.selectLanguage', + defaultMessage: 'Select code block language', + }, + 'codeblock.delete': { + id: 'designSystem.markdownEditor.codeBlock.delete', + defaultMessage: 'Delete code block', + }, + 'createLink.cancelTooltip': { + id: 'designSystem.markdownEditor.createLink.cancelTooltip', + defaultMessage: 'Cancel change', + }, + 'createLink.saveTooltip': { + id: 'designSystem.markdownEditor.createLink.saveTooltip', + defaultMessage: 'Set URL', + }, + 'createLink.text': { + id: 'designSystem.markdownEditor.createLink.text', + defaultMessage: 'Anchor text', + }, + 'createLink.textTooltip': { + id: 'designSystem.markdownEditor.createLink.textTooltip', + defaultMessage: 'The text to be displayed for the link', + }, + 'createLink.url': { id: 'designSystem.markdownEditor.createLink.url', defaultMessage: 'URL' }, + 'createLink.urlPlaceholder': { + id: 'designSystem.markdownEditor.createLink.urlPlaceholder', + defaultMessage: 'Select or paste an URL', + }, + 'dialogControls.cancel': { id: 'common.cancel', defaultMessage: 'Cancel' }, + 'dialogControls.save': { id: 'common.save', defaultMessage: 'Save' }, + 'imageEditor.deleteImage': { + id: 'designSystem.markdownEditor.imageEditor.deleteImage', + defaultMessage: 'Delete image', + }, + 'imageEditor.editImage': { + id: 'designSystem.markdownEditor.imageEditor.editImage', + defaultMessage: 'Edit image', + }, + 'linkPreview.copied': { + id: 'designSystem.markdownEditor.linkPreview.copied', + defaultMessage: 'Copied!', + }, + 'linkPreview.copyToClipboard': { + id: 'designSystem.markdownEditor.linkPreview.copyToClipboard', + defaultMessage: 'Copy to clipboard', + }, + 'linkPreview.edit': { + id: 'designSystem.markdownEditor.linkPreview.edit', + defaultMessage: 'Edit link URL', + }, + 'linkPreview.open': { + id: 'designSystem.markdownEditor.linkPreview.open', + defaultMessage: 'Open {url} in new window', + }, + 'linkPreview.remove': { + id: 'designSystem.markdownEditor.linkPreview.remove', + defaultMessage: 'Remove link', + }, + 'table.alignCenter': { + id: 'designSystem.markdownEditor.table.alignCenter', + defaultMessage: 'Align center', + }, + 'table.alignLeft': { + id: 'designSystem.markdownEditor.table.alignLeft', + defaultMessage: 'Align left', + }, + 'table.alignRight': { + id: 'designSystem.markdownEditor.table.alignRight', + defaultMessage: 'Align right', + }, + 'table.columnMenu': { + id: 'designSystem.markdownEditor.table.columnMenu', + defaultMessage: 'Column menu', + }, + 'table.deleteColumn': { + id: 'designSystem.markdownEditor.table.deleteColumn', + defaultMessage: 'Delete this column', + }, + 'table.deleteRow': { + id: 'designSystem.markdownEditor.table.deleteRow', + defaultMessage: 'Delete this row', + }, + 'table.deleteTable': { + id: 'designSystem.markdownEditor.table.deleteTable', + defaultMessage: 'Delete table', + }, + 'table.insertColumnLeft': { + id: 'designSystem.markdownEditor.table.insertColumnLeft', + defaultMessage: 'Insert a column to the left of this one', + }, + 'table.insertColumnRight': { + id: 'designSystem.markdownEditor.table.insertColumnRight', + defaultMessage: 'Insert a column to the right of this one', + }, + 'table.insertRowAbove': { + id: 'designSystem.markdownEditor.table.insertRowAbove', + defaultMessage: 'Insert a row above this one', + }, + 'table.insertRowBelow': { + id: 'designSystem.markdownEditor.table.insertRowBelow', + defaultMessage: 'Insert a row below this one', + }, + 'table.rowMenu': { id: 'designSystem.markdownEditor.table.rowMenu', defaultMessage: 'Row menu' }, + 'table.textAlignment': { + id: 'designSystem.markdownEditor.table.textAlignment', + defaultMessage: 'Text alignment', + }, + 'toolbar.bold': { id: 'designSystem.markdownEditor.toolbar.bold', defaultMessage: 'Bold' }, + 'toolbar.bulletedList': { + id: 'designSystem.markdownEditor.toolbar.bulletedList', + defaultMessage: 'Bulleted list', + }, + 'toolbar.checkList': { + id: 'designSystem.markdownEditor.toolbar.checkList', + defaultMessage: 'Check list', + }, + 'toolbar.codeBlock': { + id: 'designSystem.markdownEditor.toolbar.codeBlock', + defaultMessage: 'Insert Code Block', + }, + 'toolbar.diffMode': { + id: 'designSystem.markdownEditor.toolbar.diffMode', + defaultMessage: 'Diff mode', + }, + 'toolbar.image': { + id: 'designSystem.markdownEditor.toolbar.image', + defaultMessage: 'Insert image', + }, + 'toolbar.inlineCode': { + id: 'designSystem.markdownEditor.toolbar.inlineCode', + defaultMessage: 'Inline code format', + }, + 'toolbar.italic': { id: 'designSystem.markdownEditor.toolbar.italic', defaultMessage: 'Italic' }, + 'toolbar.link': { id: 'designSystem.markdownEditor.toolbar.link', defaultMessage: 'Create link' }, + 'toolbar.numberedList': { + id: 'designSystem.markdownEditor.toolbar.numberedList', + defaultMessage: 'Numbered list', + }, + 'toolbar.redo': { + id: 'designSystem.markdownEditor.toolbar.redo', + defaultMessage: 'Redo {shortcut}', + }, + 'toolbar.removeBold': { + id: 'designSystem.markdownEditor.toolbar.removeBold', + defaultMessage: 'Remove bold', + }, + 'toolbar.removeInlineCode': { + id: 'designSystem.markdownEditor.toolbar.removeInlineCode', + defaultMessage: 'Remove code format', + }, + 'toolbar.removeItalic': { + id: 'designSystem.markdownEditor.toolbar.removeItalic', + defaultMessage: 'Remove italic', + }, + 'toolbar.removeStrikethrough': { + id: 'designSystem.markdownEditor.toolbar.removeStrikethrough', + defaultMessage: 'Remove strikethrough', + }, + 'toolbar.removeSubscript': { + id: 'designSystem.markdownEditor.toolbar.removeSubscript', + defaultMessage: 'Remove subscript', + }, + 'toolbar.removeSuperscript': { + id: 'designSystem.markdownEditor.toolbar.removeSuperscript', + defaultMessage: 'Remove superscript', + }, + 'toolbar.removeUnderline': { + id: 'designSystem.markdownEditor.toolbar.removeUnderline', + defaultMessage: 'Remove underline', + }, + 'toolbar.richText': { + id: 'designSystem.markdownEditor.toolbar.richText', + defaultMessage: 'Rich text', + }, + 'toolbar.source': { + id: 'designSystem.markdownEditor.toolbar.source', + defaultMessage: 'Source mode', + }, + 'toolbar.strikethrough': { + id: 'designSystem.markdownEditor.toolbar.strikethrough', + defaultMessage: 'Strikethrough', + }, + 'toolbar.subscript': { + id: 'designSystem.markdownEditor.toolbar.subscript', + defaultMessage: 'Subscript', + }, + 'toolbar.superscript': { + id: 'designSystem.markdownEditor.toolbar.superscript', + defaultMessage: 'Superscript', + }, + 'toolbar.table': { + id: 'designSystem.markdownEditor.toolbar.table', + defaultMessage: 'Insert Table', + }, + 'toolbar.thematicBreak': { + id: 'designSystem.markdownEditor.toolbar.thematicBreak', + defaultMessage: 'Insert thematic break', + }, + 'toolbar.underline': { + id: 'designSystem.markdownEditor.toolbar.underline', + defaultMessage: 'Underline', + }, + 'toolbar.undo': { + id: 'designSystem.markdownEditor.toolbar.undo', + defaultMessage: 'Undo {shortcut}', + }, + 'uploadImage.addViaUrlInstructions': { + id: 'designSystem.markdownEditor.uploadImage.addViaUrlInstructions', + defaultMessage: 'Or add an image from an URL:', + }, + 'uploadImage.addViaUrlInstructionsNoUpload': { + id: 'designSystem.markdownEditor.uploadImage.addViaUrlInstructionsNoUpload', + defaultMessage: 'Add an image from an URL:', + }, + 'uploadImage.alt': { id: 'designSystem.markdownEditor.uploadImage.alt', defaultMessage: 'Alt:' }, + 'uploadImage.autoCompletePlaceholder': { + id: 'designSystem.markdownEditor.uploadImage.autoCompletePlaceholder', + defaultMessage: 'Select or paste an image src', + }, + 'uploadImage.dialogTitle': { + id: 'designSystem.markdownEditor.uploadImage.dialogTitle', + defaultMessage: 'Upload an image', + }, + 'uploadImage.height': { + id: 'designSystem.markdownEditor.uploadImage.height', + defaultMessage: 'Height:', + }, + 'uploadImage.title': { + id: 'designSystem.markdownEditor.uploadImage.title', + defaultMessage: 'Title:', + }, + 'uploadImage.uploadInstructions': { + id: 'designSystem.markdownEditor.uploadImage.uploadInstructions', + defaultMessage: 'Upload an image from your device:', + }, + 'uploadImage.width': { + id: 'designSystem.markdownEditor.uploadImage.width', + defaultMessage: 'Width:', + }, +}) diff --git a/packages/design-system/components/web/inputs/Searchbar/index.tsx b/packages/design-system/components/web/inputs/Searchbar/index.tsx index fe5ce0a83..381dabaf4 100644 --- a/packages/design-system/components/web/inputs/Searchbar/index.tsx +++ b/packages/design-system/components/web/inputs/Searchbar/index.tsx @@ -5,6 +5,7 @@ import { FC } from 'react' import { withController } from '@baseapp-frontend/utils' import { CircularProgress, InputAdornment } from '@mui/material' +import { useIntl } from 'react-intl' import IconButton from '../../buttons/IconButton' import Iconify from '../../images/Iconify' @@ -20,56 +21,63 @@ const Searchbar: FC = ({ InputProps, variant = 'filled', ...props -}) => ( - - {isPending ? ( - - - - ) : ( - - )} - - ), - endAdornment: ( - - {onClear && props.value ? ( - - - - ) : ( -
- )} - - ), - sx: { - '& .MuiFilledInput-input': { - height: '17px', - paddingTop: '14px', - paddingBottom: '14px', - paddingRight: '14px', - maxHeight: '17px', +}) => { + const intl = useIntl() + + return ( + + {isPending ? ( + + + + ) : ( + + )} + + ), + endAdornment: ( + + {onClear && props.value ? ( + + + + ) : ( +
+ )} + + ), + sx: { + '& .MuiFilledInput-input': { + height: '17px', + paddingTop: '14px', + paddingBottom: '14px', + paddingRight: '14px', + maxHeight: '17px', + }, + '& .MuiInputAdornment-positionStart': { + marginTop: '0 !important', + }, }, - }, - ...InputProps, - }} - {...props} - /> -) + ...InputProps, + }} + {...props} + /> + ) +} export default withController(Searchbar, { shouldDebounce: true }) diff --git a/packages/design-system/components/web/inputs/SelectField/__tests__/SelectField.test.tsx b/packages/design-system/components/web/inputs/SelectField/__tests__/SelectField.test.tsx new file mode 100644 index 000000000..49c9a10ea --- /dev/null +++ b/packages/design-system/components/web/inputs/SelectField/__tests__/SelectField.test.tsx @@ -0,0 +1,35 @@ +import { ThemeProvider, createTheme } from '@mui/material' +import { render, screen, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { useForm } from 'react-hook-form' + +import SelectField from '..' + +const OPTIONS = [ + { value: 'en', label: 'English' }, + { value: 'es', label: 'Español' }, +] + +let formValues: () => { language: string } + +const Form = () => { + const { control, getValues } = useForm({ defaultValues: { language: 'en' } }) + formValues = getValues + return +} + +describe('SelectField', () => { + it('stores the selected option value in the form', async () => { + render( + +
+ , + ) + + await userEvent.click(screen.getByRole('combobox')) + await userEvent.click(within(screen.getByRole('listbox')).getByText('Español')) + + expect(formValues().language).toBe('es') + expect(screen.getByRole('combobox').textContent).toBe('Español') + }) +}) diff --git a/packages/design-system/components/web/inputs/SelectField/index.tsx b/packages/design-system/components/web/inputs/SelectField/index.tsx index e4a7d945a..26e30e813 100644 --- a/packages/design-system/components/web/inputs/SelectField/index.tsx +++ b/packages/design-system/components/web/inputs/SelectField/index.tsx @@ -8,11 +8,23 @@ import { TextField as MUITextField, MenuItem, Theme, useMediaQuery } from '@mui/ import { PureSelectFieldProps, SelectFieldProps } from './types' -const SelectField: FC = ({ isResponsive = true, options, ...props }) => { +const SelectField: FC = ({ + isResponsive = true, + options, + onChange, + ...props +}) => { const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm')) return ( - + onChange?.(event)} + {...props} + > {options.map((option) => ( {option.label} diff --git a/packages/design-system/components/web/inputs/SocialTextField/index.tsx b/packages/design-system/components/web/inputs/SocialTextField/index.tsx index 222a6e05e..98c28b0f9 100644 --- a/packages/design-system/components/web/inputs/SocialTextField/index.tsx +++ b/packages/design-system/components/web/inputs/SocialTextField/index.tsx @@ -2,7 +2,7 @@ import { FC } from 'react' -import { Typography } from '@mui/material' +import { FormattedMessage, useIntl } from 'react-intl' import IconButton from '../../buttons/IconButton' import CloseIcon from '../../icons/CloseIcon' @@ -18,6 +18,7 @@ import { ReplyBar, } from './styled' import { SocialTextFieldProps } from './types' +import { renderReplyingToLabel } from './utils' /** * This is a TextField component made for comments creation. @@ -44,6 +45,8 @@ const SocialTextField: FC = ({ TextareaField = DefaultTextareaField, ...props }) => { + const intl = useIntl() + const renderField = () => { if (mode === 'rich-text') { return ( @@ -65,14 +68,26 @@ const SocialTextField: FC = ({ - - Replying to - - - {replyTargetName} - + + {replyTargetName} + + ), + }} + /> - + diff --git a/packages/design-system/components/web/inputs/SocialTextField/utils.tsx b/packages/design-system/components/web/inputs/SocialTextField/utils.tsx new file mode 100644 index 000000000..a14bffcd5 --- /dev/null +++ b/packages/design-system/components/web/inputs/SocialTextField/utils.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from 'react' + +import { Typography } from '@mui/material' + +export const renderReplyingToLabel = (chunks: ReactNode[]) => ( + + {chunks} + +) diff --git a/packages/design-system/components/web/logos/CustomLogoCondensed/index.tsx b/packages/design-system/components/web/logos/CustomLogoCondensed/index.tsx index acec61e39..2c6048157 100644 --- a/packages/design-system/components/web/logos/CustomLogoCondensed/index.tsx +++ b/packages/design-system/components/web/logos/CustomLogoCondensed/index.tsx @@ -3,10 +3,12 @@ import { FC } from 'react' import Image from 'next/image' +import { useIntl } from 'react-intl' import { useLogoOverrides } from '../../../../hooks/web' const CustomLogoCondensed: FC = () => { + const intl = useIntl() const { logos } = useLogoOverrides() if (!logos?.square) { return null @@ -15,7 +17,10 @@ const CustomLogoCondensed: FC = () => { Custom Project Logo Condensed diff --git a/packages/design-system/locales/en.json b/packages/design-system/locales/en.json new file mode 100644 index 000000000..c10e36820 --- /dev/null +++ b/packages/design-system/locales/en.json @@ -0,0 +1,87 @@ +{ + "designSystem.autocompleteField.placeholder": "Search", + "designSystem.avatar.deletedUser": "Deleted User Avatar", + "designSystem.avatar.fallback": "Avatar Fallback", + "designSystem.customLogoCondensed.alt": "Custom Project Logo Condensed", + "designSystem.dropzone.actionText": "Upload Image", + "designSystem.dropzone.fileNotAccepted": "File not accepted, please choose the correct type", + "designSystem.dropzone.fileTooLarge": "This file is too large (max {maxFileSize} MB).", + "designSystem.dropzone.inputAriaLabel": "Drag and drop files to upload", + "designSystem.dropzone.maxFileSize": "Max. File Size: {maxFileSize}MB", + "designSystem.dropzone.preview.imageAlt": "preview", + "designSystem.dropzone.preview.removeFile": "Remove file", + "designSystem.dropzone.remove": "Remove", + "designSystem.dropzone.title": "Click to browse or drag and drop.", + "designSystem.fileUploadButton.fileTooLarge": "This file is too large (max {maxSize}MB).", + "designSystem.fileUploadButton.label": "Upload File", + "designSystem.markdownEditor.codeBlock.delete": "Delete code block", + "designSystem.markdownEditor.codeBlock.inlineLanguage": "Language", + "designSystem.markdownEditor.codeBlock.language": "Code block language", + "designSystem.markdownEditor.codeBlock.selectLanguage": "Select code block language", + "designSystem.markdownEditor.createLink.cancelTooltip": "Cancel change", + "designSystem.markdownEditor.createLink.saveTooltip": "Set URL", + "designSystem.markdownEditor.createLink.text": "Anchor text", + "designSystem.markdownEditor.createLink.textTooltip": "The text to be displayed for the link", + "designSystem.markdownEditor.createLink.url": "URL", + "designSystem.markdownEditor.createLink.urlPlaceholder": "Select or paste an URL", + "designSystem.markdownEditor.imageEditor.deleteImage": "Delete image", + "designSystem.markdownEditor.imageEditor.editImage": "Edit image", + "designSystem.markdownEditor.linkPreview.copied": "Copied!", + "designSystem.markdownEditor.linkPreview.copyToClipboard": "Copy to clipboard", + "designSystem.markdownEditor.linkPreview.edit": "Edit link URL", + "designSystem.markdownEditor.linkPreview.open": "Open {url} in new window", + "designSystem.markdownEditor.linkPreview.remove": "Remove link", + "designSystem.markdownEditor.mentions.suggestions": "Mention suggestions", + "designSystem.markdownEditor.table.alignCenter": "Align center", + "designSystem.markdownEditor.table.alignLeft": "Align left", + "designSystem.markdownEditor.table.alignRight": "Align right", + "designSystem.markdownEditor.table.columnMenu": "Column menu", + "designSystem.markdownEditor.table.deleteColumn": "Delete this column", + "designSystem.markdownEditor.table.deleteRow": "Delete this row", + "designSystem.markdownEditor.table.deleteTable": "Delete table", + "designSystem.markdownEditor.table.insertColumnLeft": "Insert a column to the left of this one", + "designSystem.markdownEditor.table.insertColumnRight": "Insert a column to the right of this one", + "designSystem.markdownEditor.table.insertRowAbove": "Insert a row above this one", + "designSystem.markdownEditor.table.insertRowBelow": "Insert a row below this one", + "designSystem.markdownEditor.table.rowMenu": "Row menu", + "designSystem.markdownEditor.table.textAlignment": "Text alignment", + "designSystem.markdownEditor.toolbar.bold": "Bold", + "designSystem.markdownEditor.toolbar.bulletedList": "Bulleted list", + "designSystem.markdownEditor.toolbar.checkList": "Check list", + "designSystem.markdownEditor.toolbar.codeBlock": "Insert Code Block", + "designSystem.markdownEditor.toolbar.diffMode": "Diff mode", + "designSystem.markdownEditor.toolbar.image": "Insert image", + "designSystem.markdownEditor.toolbar.inlineCode": "Inline code format", + "designSystem.markdownEditor.toolbar.italic": "Italic", + "designSystem.markdownEditor.toolbar.link": "Create link", + "designSystem.markdownEditor.toolbar.numberedList": "Numbered list", + "designSystem.markdownEditor.toolbar.redo": "Redo {shortcut}", + "designSystem.markdownEditor.toolbar.removeBold": "Remove bold", + "designSystem.markdownEditor.toolbar.removeInlineCode": "Remove code format", + "designSystem.markdownEditor.toolbar.removeItalic": "Remove italic", + "designSystem.markdownEditor.toolbar.removeStrikethrough": "Remove strikethrough", + "designSystem.markdownEditor.toolbar.removeSubscript": "Remove subscript", + "designSystem.markdownEditor.toolbar.removeSuperscript": "Remove superscript", + "designSystem.markdownEditor.toolbar.removeUnderline": "Remove underline", + "designSystem.markdownEditor.toolbar.richText": "Rich text", + "designSystem.markdownEditor.toolbar.source": "Source mode", + "designSystem.markdownEditor.toolbar.strikethrough": "Strikethrough", + "designSystem.markdownEditor.toolbar.subscript": "Subscript", + "designSystem.markdownEditor.toolbar.superscript": "Superscript", + "designSystem.markdownEditor.toolbar.table": "Insert Table", + "designSystem.markdownEditor.toolbar.thematicBreak": "Insert thematic break", + "designSystem.markdownEditor.toolbar.underline": "Underline", + "designSystem.markdownEditor.toolbar.undo": "Undo {shortcut}", + "designSystem.markdownEditor.uploadImage.addViaUrlInstructions": "Or add an image from an URL:", + "designSystem.markdownEditor.uploadImage.addViaUrlInstructionsNoUpload": "Add an image from an URL:", + "designSystem.markdownEditor.uploadImage.alt": "Alt:", + "designSystem.markdownEditor.uploadImage.autoCompletePlaceholder": "Select or paste an image src", + "designSystem.markdownEditor.uploadImage.dialogTitle": "Upload an image", + "designSystem.markdownEditor.uploadImage.height": "Height:", + "designSystem.markdownEditor.uploadImage.title": "Title:", + "designSystem.markdownEditor.uploadImage.uploadInstructions": "Upload an image from your device:", + "designSystem.markdownEditor.uploadImage.width": "Width:", + "designSystem.searchbar.placeholder": "Search", + "designSystem.socialTextField.cancelReply": "cancel reply", + "designSystem.socialTextField.replyingTo": " {name}" +} diff --git a/packages/design-system/locales/es.json b/packages/design-system/locales/es.json new file mode 100644 index 000000000..8438ca327 --- /dev/null +++ b/packages/design-system/locales/es.json @@ -0,0 +1,87 @@ +{ + "designSystem.autocompleteField.placeholder": "Buscar", + "designSystem.avatar.deletedUser": "Avatar de usuario eliminado", + "designSystem.avatar.fallback": "Avatar predeterminado", + "designSystem.customLogoCondensed.alt": "Logo condensado del proyecto", + "designSystem.dropzone.actionText": "Subir imagen", + "designSystem.dropzone.fileNotAccepted": "Archivo no aceptado, elige el tipo correcto", + "designSystem.dropzone.fileTooLarge": "Este archivo es demasiado grande (máx. {maxFileSize} MB).", + "designSystem.dropzone.inputAriaLabel": "Arrastra y suelta archivos para subirlos", + "designSystem.dropzone.maxFileSize": "Tamaño máx. de archivo: {maxFileSize}MB", + "designSystem.dropzone.preview.imageAlt": "vista previa", + "designSystem.dropzone.preview.removeFile": "Quitar archivo", + "designSystem.dropzone.remove": "Quitar", + "designSystem.dropzone.title": "Haz clic para buscar o arrastra y suelta.", + "designSystem.fileUploadButton.fileTooLarge": "Este archivo es demasiado grande (máx. {maxSize}MB).", + "designSystem.fileUploadButton.label": "Subir archivo", + "designSystem.markdownEditor.codeBlock.delete": "Eliminar bloque de código", + "designSystem.markdownEditor.codeBlock.inlineLanguage": "Lenguaje", + "designSystem.markdownEditor.codeBlock.language": "Lenguaje del bloque de código", + "designSystem.markdownEditor.codeBlock.selectLanguage": "Selecciona el lenguaje del bloque de código", + "designSystem.markdownEditor.createLink.cancelTooltip": "Cancelar cambio", + "designSystem.markdownEditor.createLink.saveTooltip": "Establecer URL", + "designSystem.markdownEditor.createLink.text": "Texto del enlace", + "designSystem.markdownEditor.createLink.textTooltip": "El texto que se mostrará para el enlace", + "designSystem.markdownEditor.createLink.url": "URL", + "designSystem.markdownEditor.createLink.urlPlaceholder": "Selecciona o pega una URL", + "designSystem.markdownEditor.imageEditor.deleteImage": "Eliminar imagen", + "designSystem.markdownEditor.imageEditor.editImage": "Editar imagen", + "designSystem.markdownEditor.linkPreview.copied": "¡Copiado!", + "designSystem.markdownEditor.linkPreview.copyToClipboard": "Copiar al portapapeles", + "designSystem.markdownEditor.linkPreview.edit": "Editar URL del enlace", + "designSystem.markdownEditor.linkPreview.open": "Abrir {url} en una ventana nueva", + "designSystem.markdownEditor.linkPreview.remove": "Quitar enlace", + "designSystem.markdownEditor.mentions.suggestions": "Sugerencias de menciones", + "designSystem.markdownEditor.table.alignCenter": "Centrar", + "designSystem.markdownEditor.table.alignLeft": "Alinear a la izquierda", + "designSystem.markdownEditor.table.alignRight": "Alinear a la derecha", + "designSystem.markdownEditor.table.columnMenu": "Menú de columna", + "designSystem.markdownEditor.table.deleteColumn": "Eliminar esta columna", + "designSystem.markdownEditor.table.deleteRow": "Eliminar esta fila", + "designSystem.markdownEditor.table.deleteTable": "Eliminar tabla", + "designSystem.markdownEditor.table.insertColumnLeft": "Insertar una columna a la izquierda de esta", + "designSystem.markdownEditor.table.insertColumnRight": "Insertar una columna a la derecha de esta", + "designSystem.markdownEditor.table.insertRowAbove": "Insertar una fila encima de esta", + "designSystem.markdownEditor.table.insertRowBelow": "Insertar una fila debajo de esta", + "designSystem.markdownEditor.table.rowMenu": "Menú de fila", + "designSystem.markdownEditor.table.textAlignment": "Alineación del texto", + "designSystem.markdownEditor.toolbar.bold": "Negrita", + "designSystem.markdownEditor.toolbar.bulletedList": "Lista con viñetas", + "designSystem.markdownEditor.toolbar.checkList": "Lista de verificación", + "designSystem.markdownEditor.toolbar.codeBlock": "Insertar bloque de código", + "designSystem.markdownEditor.toolbar.diffMode": "Modo de diferencias", + "designSystem.markdownEditor.toolbar.image": "Insertar imagen", + "designSystem.markdownEditor.toolbar.inlineCode": "Formato de código en línea", + "designSystem.markdownEditor.toolbar.italic": "Cursiva", + "designSystem.markdownEditor.toolbar.link": "Crear enlace", + "designSystem.markdownEditor.toolbar.numberedList": "Lista numerada", + "designSystem.markdownEditor.toolbar.redo": "Rehacer {shortcut}", + "designSystem.markdownEditor.toolbar.removeBold": "Quitar negrita", + "designSystem.markdownEditor.toolbar.removeInlineCode": "Quitar formato de código", + "designSystem.markdownEditor.toolbar.removeItalic": "Quitar cursiva", + "designSystem.markdownEditor.toolbar.removeStrikethrough": "Quitar tachado", + "designSystem.markdownEditor.toolbar.removeSubscript": "Quitar subíndice", + "designSystem.markdownEditor.toolbar.removeSuperscript": "Quitar superíndice", + "designSystem.markdownEditor.toolbar.removeUnderline": "Quitar subrayado", + "designSystem.markdownEditor.toolbar.richText": "Texto enriquecido", + "designSystem.markdownEditor.toolbar.source": "Modo código fuente", + "designSystem.markdownEditor.toolbar.strikethrough": "Tachado", + "designSystem.markdownEditor.toolbar.subscript": "Subíndice", + "designSystem.markdownEditor.toolbar.superscript": "Superíndice", + "designSystem.markdownEditor.toolbar.table": "Insertar tabla", + "designSystem.markdownEditor.toolbar.thematicBreak": "Insertar separador", + "designSystem.markdownEditor.toolbar.underline": "Subrayado", + "designSystem.markdownEditor.toolbar.undo": "Deshacer {shortcut}", + "designSystem.markdownEditor.uploadImage.addViaUrlInstructions": "O agrega una imagen desde una URL:", + "designSystem.markdownEditor.uploadImage.addViaUrlInstructionsNoUpload": "Agrega una imagen desde una URL:", + "designSystem.markdownEditor.uploadImage.alt": "Texto alternativo:", + "designSystem.markdownEditor.uploadImage.autoCompletePlaceholder": "Selecciona o pega la fuente de una imagen", + "designSystem.markdownEditor.uploadImage.dialogTitle": "Subir una imagen", + "designSystem.markdownEditor.uploadImage.height": "Alto:", + "designSystem.markdownEditor.uploadImage.title": "Título:", + "designSystem.markdownEditor.uploadImage.uploadInstructions": "Sube una imagen desde tu dispositivo:", + "designSystem.markdownEditor.uploadImage.width": "Ancho:", + "designSystem.searchbar.placeholder": "Buscar", + "designSystem.socialTextField.cancelReply": "cancelar respuesta", + "designSystem.socialTextField.replyingTo": " {name}" +} diff --git a/packages/design-system/locales/index.ts b/packages/design-system/locales/index.ts new file mode 100644 index 000000000..78ecf303c --- /dev/null +++ b/packages/design-system/locales/index.ts @@ -0,0 +1,7 @@ +/** + * Export all locale files for dynamic loading + * This allows the locale loader to automatically import them + */ +export { default as en } from './en.json' +export { default as es } from './es.json' +export { default as pt } from './pt.json' diff --git a/packages/design-system/locales/pt.json b/packages/design-system/locales/pt.json new file mode 100644 index 000000000..19404afdd --- /dev/null +++ b/packages/design-system/locales/pt.json @@ -0,0 +1,87 @@ +{ + "designSystem.autocompleteField.placeholder": "Pesquisar", + "designSystem.avatar.deletedUser": "Avatar de usuário excluído", + "designSystem.avatar.fallback": "Avatar padrão", + "designSystem.customLogoCondensed.alt": "Logo condensado do projeto", + "designSystem.dropzone.actionText": "Enviar imagem", + "designSystem.dropzone.fileNotAccepted": "Arquivo não aceito, escolha o tipo correto", + "designSystem.dropzone.fileTooLarge": "Este arquivo é muito grande (máx. {maxFileSize} MB).", + "designSystem.dropzone.inputAriaLabel": "Arraste e solte arquivos para enviar", + "designSystem.dropzone.maxFileSize": "Tamanho máx. do arquivo: {maxFileSize}MB", + "designSystem.dropzone.preview.imageAlt": "pré-visualização", + "designSystem.dropzone.preview.removeFile": "Remover arquivo", + "designSystem.dropzone.remove": "Remover", + "designSystem.dropzone.title": "Clique para procurar ou arraste e solte.", + "designSystem.fileUploadButton.fileTooLarge": "Este arquivo é muito grande (máx. {maxSize}MB).", + "designSystem.fileUploadButton.label": "Enviar arquivo", + "designSystem.markdownEditor.codeBlock.delete": "Excluir bloco de código", + "designSystem.markdownEditor.codeBlock.inlineLanguage": "Linguagem", + "designSystem.markdownEditor.codeBlock.language": "Linguagem do bloco de código", + "designSystem.markdownEditor.codeBlock.selectLanguage": "Selecione a linguagem do bloco de código", + "designSystem.markdownEditor.createLink.cancelTooltip": "Cancelar alteração", + "designSystem.markdownEditor.createLink.saveTooltip": "Definir URL", + "designSystem.markdownEditor.createLink.text": "Texto do link", + "designSystem.markdownEditor.createLink.textTooltip": "O texto a ser exibido para o link", + "designSystem.markdownEditor.createLink.url": "URL", + "designSystem.markdownEditor.createLink.urlPlaceholder": "Selecione ou cole uma URL", + "designSystem.markdownEditor.imageEditor.deleteImage": "Excluir imagem", + "designSystem.markdownEditor.imageEditor.editImage": "Editar imagem", + "designSystem.markdownEditor.linkPreview.copied": "Copiado!", + "designSystem.markdownEditor.linkPreview.copyToClipboard": "Copiar para a área de transferência", + "designSystem.markdownEditor.linkPreview.edit": "Editar URL do link", + "designSystem.markdownEditor.linkPreview.open": "Abrir {url} em uma nova janela", + "designSystem.markdownEditor.linkPreview.remove": "Remover link", + "designSystem.markdownEditor.mentions.suggestions": "Sugestões de menções", + "designSystem.markdownEditor.table.alignCenter": "Centralizar", + "designSystem.markdownEditor.table.alignLeft": "Alinhar à esquerda", + "designSystem.markdownEditor.table.alignRight": "Alinhar à direita", + "designSystem.markdownEditor.table.columnMenu": "Menu da coluna", + "designSystem.markdownEditor.table.deleteColumn": "Excluir esta coluna", + "designSystem.markdownEditor.table.deleteRow": "Excluir esta linha", + "designSystem.markdownEditor.table.deleteTable": "Excluir tabela", + "designSystem.markdownEditor.table.insertColumnLeft": "Inserir uma coluna à esquerda desta", + "designSystem.markdownEditor.table.insertColumnRight": "Inserir uma coluna à direita desta", + "designSystem.markdownEditor.table.insertRowAbove": "Inserir uma linha acima desta", + "designSystem.markdownEditor.table.insertRowBelow": "Inserir uma linha abaixo desta", + "designSystem.markdownEditor.table.rowMenu": "Menu da linha", + "designSystem.markdownEditor.table.textAlignment": "Alinhamento do texto", + "designSystem.markdownEditor.toolbar.bold": "Negrito", + "designSystem.markdownEditor.toolbar.bulletedList": "Lista com marcadores", + "designSystem.markdownEditor.toolbar.checkList": "Lista de verificação", + "designSystem.markdownEditor.toolbar.codeBlock": "Inserir bloco de código", + "designSystem.markdownEditor.toolbar.diffMode": "Modo de diferenças", + "designSystem.markdownEditor.toolbar.image": "Inserir imagem", + "designSystem.markdownEditor.toolbar.inlineCode": "Formato de código em linha", + "designSystem.markdownEditor.toolbar.italic": "Itálico", + "designSystem.markdownEditor.toolbar.link": "Criar link", + "designSystem.markdownEditor.toolbar.numberedList": "Lista numerada", + "designSystem.markdownEditor.toolbar.redo": "Refazer {shortcut}", + "designSystem.markdownEditor.toolbar.removeBold": "Remover negrito", + "designSystem.markdownEditor.toolbar.removeInlineCode": "Remover formato de código", + "designSystem.markdownEditor.toolbar.removeItalic": "Remover itálico", + "designSystem.markdownEditor.toolbar.removeStrikethrough": "Remover tachado", + "designSystem.markdownEditor.toolbar.removeSubscript": "Remover subscrito", + "designSystem.markdownEditor.toolbar.removeSuperscript": "Remover sobrescrito", + "designSystem.markdownEditor.toolbar.removeUnderline": "Remover sublinhado", + "designSystem.markdownEditor.toolbar.richText": "Texto formatado", + "designSystem.markdownEditor.toolbar.source": "Modo código-fonte", + "designSystem.markdownEditor.toolbar.strikethrough": "Tachado", + "designSystem.markdownEditor.toolbar.subscript": "Subscrito", + "designSystem.markdownEditor.toolbar.superscript": "Sobrescrito", + "designSystem.markdownEditor.toolbar.table": "Inserir tabela", + "designSystem.markdownEditor.toolbar.thematicBreak": "Inserir separador", + "designSystem.markdownEditor.toolbar.underline": "Sublinhado", + "designSystem.markdownEditor.toolbar.undo": "Desfazer {shortcut}", + "designSystem.markdownEditor.uploadImage.addViaUrlInstructions": "Ou adicione uma imagem a partir de uma URL:", + "designSystem.markdownEditor.uploadImage.addViaUrlInstructionsNoUpload": "Adicione uma imagem a partir de uma URL:", + "designSystem.markdownEditor.uploadImage.alt": "Texto alternativo:", + "designSystem.markdownEditor.uploadImage.autoCompletePlaceholder": "Selecione ou cole a origem de uma imagem", + "designSystem.markdownEditor.uploadImage.dialogTitle": "Enviar uma imagem", + "designSystem.markdownEditor.uploadImage.height": "Altura:", + "designSystem.markdownEditor.uploadImage.title": "Título:", + "designSystem.markdownEditor.uploadImage.uploadInstructions": "Envie uma imagem do seu dispositivo:", + "designSystem.markdownEditor.uploadImage.width": "Largura:", + "designSystem.searchbar.placeholder": "Pesquisar", + "designSystem.socialTextField.cancelReply": "cancelar resposta", + "designSystem.socialTextField.replyingTo": " {name}" +} diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 97a6b749e..c9796834d 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -15,6 +15,7 @@ }, "exports": { "./package.json": "./package.json", + "./locales": "./locales/index.ts", "./components/native/appbars": "./components/native/appbars/index.ts", "./components/native/avatars": "./components/native/avatars/index.ts", "./components/native/badges": "./components/native/badges/index.ts", @@ -81,6 +82,7 @@ "components", "hooks", "layouts", + "locales", "providers", "styles", "tests", @@ -120,6 +122,7 @@ "next": "catalog:", "react-dropzone": "catalog:design-system", "react-hook-form": "catalog:", + "react-intl": "catalog:", "react-markdown": "catalog:design-system", "rehype-raw": "catalog:design-system", "remark-gfm": "catalog:design-system", diff --git a/packages/design-system/tests/web/utils/withDesignSystemTestProviders/index.tsx b/packages/design-system/tests/web/utils/withDesignSystemTestProviders/index.tsx index 667e65502..e9abee815 100644 --- a/packages/design-system/tests/web/utils/withDesignSystemTestProviders/index.tsx +++ b/packages/design-system/tests/web/utils/withDesignSystemTestProviders/index.tsx @@ -4,8 +4,10 @@ import { NotificationProvider } from '@baseapp-frontend/utils' import createCache from '@emotion/cache' import { CacheProvider } from '@emotion/react' +import { IntlProvider } from 'react-intl' import { UISettingsProvider } from '../../../../hooks/web' +import enMessages from '../../../../locales/en.json' import { SnackbarProvider } from '../../../../providers/web' import { createPalette } from '../../../../styles/web/palette' import ReactSuspenseWithLoading from '../ReactSuspenseWithLoading' @@ -24,22 +26,24 @@ const withDesignSystemProvider = const emotionCache = createCache({ key: 'test-key' }) return ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ) } diff --git a/packages/design-system/tsconfig.json b/packages/design-system/tsconfig.json index 9eab8bd9a..995a97560 100644 --- a/packages/design-system/tsconfig.json +++ b/packages/design-system/tsconfig.json @@ -4,6 +4,15 @@ "types": ["node", "vitest/globals"], "outDir": "./dist" }, - "include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", ".eslintrc.js", "*.ts", "*.tsx", "index.d.ts"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/*.d.ts", + "locales/*.json", + ".eslintrc.js", + "*.ts", + "*.tsx", + "index.d.ts" + ], "exclude": ["node_modules", "dist"] } diff --git a/packages/i18n/locales/en.json b/packages/i18n/locales/en.json index d2bda0b29..0ae4d263d 100644 --- a/packages/i18n/locales/en.json +++ b/packages/i18n/locales/en.json @@ -12,22 +12,5 @@ "common.error": "Error", "common.success": "Success", "common.warning": "Warning", - "common.info": "Info", - "settings.title": "Settings", - "settings.tabs.profile": "Profile", - "settings.tabs.credentials": "Credentials", - "settings.tabs.preferences": "Preferences", - "settings.tabs.theme": "Theme", - "settings.tabs.notifications": "Notifications", - "settings.tabs.members": "Members", - "settings.tabs.deleteAccount": "Delete Account", - "settings.preferences.title": "Preferences", - "settings.preferences.description": "Customize your language and timezone preferences.", - "settings.preferences.languageLabel": "Language", - "settings.preferences.languageHelper": "Select your preferred language for the application", - "settings.preferences.timezoneLabel": "Timezone", - "settings.preferences.timezoneHelper": "Select your timezone for displaying dates and times", - "settings.preferences.saveButton": "Save Changes", - "settings.preferences.saveSuccess": "Preferences saved successfully", - "settings.preferences.saveError": "Failed to save preferences" + "common.info": "Info" } diff --git a/packages/i18n/locales/es.json b/packages/i18n/locales/es.json index 000e64ab7..016c5e967 100644 --- a/packages/i18n/locales/es.json +++ b/packages/i18n/locales/es.json @@ -12,22 +12,5 @@ "common.error": "Error", "common.success": "Éxito", "common.warning": "Advertencia", - "common.info": "Información", - "settings.title": "Configuración", - "settings.tabs.profile": "Perfil", - "settings.tabs.credentials": "Credenciales", - "settings.tabs.preferences": "Preferencias", - "settings.tabs.theme": "Tema", - "settings.tabs.notifications": "Notificaciones", - "settings.tabs.members": "Miembros", - "settings.tabs.deleteAccount": "Eliminar cuenta", - "settings.preferences.title": "Preferencias", - "settings.preferences.description": "Personaliza tus preferencias de idioma y zona horaria.", - "settings.preferences.languageLabel": "Idioma", - "settings.preferences.languageHelper": "Selecciona tu idioma preferido para la aplicación", - "settings.preferences.timezoneLabel": "Zona horaria", - "settings.preferences.timezoneHelper": "Selecciona tu zona horaria para mostrar fechas y horas", - "settings.preferences.saveButton": "Guardar cambios", - "settings.preferences.saveSuccess": "Preferencias guardadas correctamente", - "settings.preferences.saveError": "No se pudieron guardar las preferencias" + "common.info": "Información" } diff --git a/packages/i18n/locales/pt.json b/packages/i18n/locales/pt.json index 859479e51..31882a476 100644 --- a/packages/i18n/locales/pt.json +++ b/packages/i18n/locales/pt.json @@ -12,22 +12,5 @@ "common.error": "Erro", "common.success": "Sucesso", "common.warning": "Aviso", - "common.info": "Informação", - "settings.title": "Configurações", - "settings.tabs.profile": "Perfil", - "settings.tabs.credentials": "Credenciais", - "settings.tabs.preferences": "Preferências", - "settings.tabs.theme": "Tema", - "settings.tabs.notifications": "Notificações", - "settings.tabs.members": "Membros", - "settings.tabs.deleteAccount": "Excluir conta", - "settings.preferences.title": "Preferências", - "settings.preferences.description": "Personalize suas preferências de idioma e fuso horário.", - "settings.preferences.languageLabel": "Idioma", - "settings.preferences.languageHelper": "Selecione o idioma de sua preferência para o aplicativo", - "settings.preferences.timezoneLabel": "Fuso horário", - "settings.preferences.timezoneHelper": "Selecione seu fuso horário para exibir datas e horas", - "settings.preferences.saveButton": "Salvar alterações", - "settings.preferences.saveSuccess": "Preferências salvas com sucesso", - "settings.preferences.saveError": "Não foi possível salvar as preferências" + "common.info": "Informação" } diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 7c2e04dbd..3e3938127 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -28,7 +28,7 @@ "@baseapp-frontend/config": "workspace:*", "@baseapp-frontend/eslint-plugin": "workspace:*", "@baseapp-frontend/tsconfig": "workspace:*", - "@formatjs/cli": "^6.7.4", + "@formatjs/cli": "catalog:", "@types/luxon": "catalog:", "@types/node": "catalog:", "@types/react": "catalog:react19", diff --git a/packages/i18n/providers/IntlProvider/index.tsx b/packages/i18n/providers/IntlProvider/index.tsx index 4a25273ca..9aed17a30 100644 --- a/packages/i18n/providers/IntlProvider/index.tsx +++ b/packages/i18n/providers/IntlProvider/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { FC, useEffect, useMemo, useState } from 'react' +import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { IntlProvider as ReactIntlProvider } from 'react-intl' @@ -14,6 +14,7 @@ const IntlProviderWrapper: FC = ({ locale: initialLocale, defaultLocale = DEFAULT_LOCALE, additionalMessages, + additionalMessagesByLocale, initialCookies, }) => { // Detect locale from cookies or use provided locale @@ -33,26 +34,29 @@ const IntlProviderWrapper: FC = ({ const [locale, setLocale] = useState(detectedLocale) - const [messages, setMessages] = useState>( - getMessages(locale, additionalMessages), + const getLocaleMessages = useCallback( + (target: Locale) => + getMessages(target, { ...additionalMessages, ...additionalMessagesByLocale?.[target] }), + [additionalMessages, additionalMessagesByLocale], ) + const messages = useMemo(() => getLocaleMessages(locale), [getLocaleMessages, locale]) + useEffect(() => { // Load locale from storage if not provided via props or cookies if (!initialLocale && !initialCookies) { - getCurrentLocale().then((currentLocale) => { - setLocale(currentLocale) - setMessages(getMessages(currentLocale, additionalMessages)) - }) + getCurrentLocale() + .then(setLocale) + // Keep the default locale if the stored language can't be read. + .catch(() => undefined) } - }, [initialLocale, initialCookies, additionalMessages]) + }, [initialLocale, initialCookies]) useEffect(() => { if (initialLocale && initialLocale !== locale) { setLocale(initialLocale) - setMessages(getMessages(initialLocale, additionalMessages)) } - }, [initialLocale, locale, additionalMessages]) + }, [initialLocale, locale]) return ( diff --git a/packages/i18n/providers/IntlProvider/types.ts b/packages/i18n/providers/IntlProvider/types.ts index f26e65ee1..1cd9a99f4 100644 --- a/packages/i18n/providers/IntlProvider/types.ts +++ b/packages/i18n/providers/IntlProvider/types.ts @@ -7,5 +7,6 @@ export interface IntlProviderWrapperProps { locale?: Locale defaultLocale?: Locale additionalMessages?: Record + additionalMessagesByLocale?: Partial>> initialCookies?: Record } diff --git a/packages/utils/constants/zod.ts b/packages/utils/constants/zod.ts index c77b17e8c..656553241 100644 --- a/packages/utils/constants/zod.ts +++ b/packages/utils/constants/zod.ts @@ -1,3 +1,5 @@ +import { defineMessages } from '@formatjs/intl' + export const ZOD_MESSAGE = { required: 'This field is required.', email: 'Please provide a properly formatted email address.', @@ -5,3 +7,23 @@ export const ZOD_MESSAGE = { password: 'Password must be at least 10 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character.', } + +export const ZOD_MESSAGE_DESCRIPTORS = defineMessages({ + required: { + id: 'utils.validation.fieldRequired', + defaultMessage: 'This field is required.', + }, + email: { + id: 'utils.validation.emailFormat', + defaultMessage: 'Please provide a properly formatted email address.', + }, + passwordDoNotMatch: { + id: 'utils.validation.passwordsDoNotMatch', + defaultMessage: 'Passwords do not match.', + }, + password: { + id: 'utils.validation.passwordRequirements', + defaultMessage: + 'Password must be at least 10 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character.', + }, +}) diff --git a/packages/utils/functions/zod/getZodMessages/index.ts b/packages/utils/functions/zod/getZodMessages/index.ts new file mode 100644 index 000000000..89e37157a --- /dev/null +++ b/packages/utils/functions/zod/getZodMessages/index.ts @@ -0,0 +1,10 @@ +import { ZOD_MESSAGE_DESCRIPTORS } from '../../../constants/zod' +import type { IntlFormatter } from '../../../types/intl' +import type { ZodMessages } from '../../../types/zod' + +export const getZodMessages = (intl: IntlFormatter): ZodMessages => ({ + required: intl.formatMessage(ZOD_MESSAGE_DESCRIPTORS.required), + email: intl.formatMessage(ZOD_MESSAGE_DESCRIPTORS.email), + passwordDoNotMatch: intl.formatMessage(ZOD_MESSAGE_DESCRIPTORS.passwordDoNotMatch), + password: intl.formatMessage(ZOD_MESSAGE_DESCRIPTORS.password), +}) diff --git a/packages/utils/functions/zod/index.ts b/packages/utils/functions/zod/index.ts new file mode 100644 index 000000000..22f4afb0e --- /dev/null +++ b/packages/utils/functions/zod/index.ts @@ -0,0 +1 @@ +export * from './getZodMessages' diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 52d237d80..95a39ab0a 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -22,6 +22,7 @@ export * from './functions/os' export * from './functions/relay' export * from './functions/string' export * from './functions/token' +export * from './functions/zod' export { default as useDebounce } from './hooks/useDebounce' export { default as useDebouncedValue } from './hooks/useDebouncedValue' @@ -39,5 +40,7 @@ export type * from './types/date' export type * from './types/django' export type * from './types/env' export type * from './types/form' +export type * from './types/intl' export type * from './types/jwt' export type * from './types/typescript' +export type * from './types/zod' diff --git a/packages/utils/locales/en.json b/packages/utils/locales/en.json index 88a7d7660..97caa4d08 100644 --- a/packages/utils/locales/en.json +++ b/packages/utils/locales/en.json @@ -1,39 +1,43 @@ { - "utils.common.loading": "Loading...", - "utils.common.error": "Error", - "utils.common.success": "Success", + "utils.common.back": "Back", "utils.common.cancel": "Cancel", + "utils.common.close": "Close", "utils.common.confirm": "Confirm", - "utils.common.save": "Save", "utils.common.delete": "Delete", "utils.common.edit": "Edit", - "utils.common.close": "Close", - "utils.common.back": "Back", + "utils.common.error": "Error", + "utils.common.filter": "Filter", + "utils.common.loading": "Loading...", "utils.common.next": "Next", "utils.common.previous": "Previous", - "utils.common.submit": "Submit", + "utils.common.save": "Save", "utils.common.search": "Search", - "utils.common.filter": "Filter", "utils.common.sort": "Sort", - "utils.common.viewMore": "View More", + "utils.common.submit": "Submit", + "utils.common.success": "Success", "utils.common.viewLess": "View Less", - "utils.date.today": "Today", - "utils.date.yesterday": "Yesterday", - "utils.date.period": "Period", - "utils.date.startDate": "Start date", + "utils.common.viewMore": "View More", "utils.date.endDate": "End date", "utils.date.from": "From {date}", - "utils.date.until": "Until {date}", + "utils.date.period": "Period", "utils.date.range": "{from} - {to}", - "utils.time.minutesAgo": "{count, plural, one {1 minute ago} other {{count} minutes ago}}", - "utils.time.hoursAgo": "{count, plural, one {1 hour ago} other {{count} hours ago}}", + "utils.date.startDate": "Start date", + "utils.date.today": "Today", + "utils.date.until": "Until {date}", + "utils.date.yesterday": "Yesterday", "utils.time.daysAgo": "{count, plural, one {1 day ago} other {{count} days ago}}", - "utils.time.weeksAgo": "{count, plural, one {1 week ago} other {{count} weeks ago}}", + "utils.time.hoursAgo": "{count, plural, one {1 hour ago} other {{count} hours ago}}", + "utils.time.minutesAgo": "{count, plural, one {1 minute ago} other {{count} minutes ago}}", "utils.time.monthsAgo": "{count, plural, one {1 month ago} other {{count} months ago}}", - "utils.validation.required": "This field is required", + "utils.time.weeksAgo": "{count, plural, one {1 week ago} other {{count} weeks ago}}", "utils.validation.email": "Please enter a valid email address", - "utils.validation.minLength": "Minimum length is {min} characters", - "utils.validation.maxLength": "Maximum length is {max} characters", + "utils.validation.emailFormat": "Please provide a properly formatted email address.", + "utils.validation.fieldRequired": "This field is required.", "utils.validation.invalidDate": "Your date is not valid", - "utils.validation.minDate": "End date cannot be earlier than start date" + "utils.validation.maxLength": "Maximum length is {max} characters", + "utils.validation.minDate": "End date cannot be earlier than start date", + "utils.validation.minLength": "Minimum length is {min} characters", + "utils.validation.passwordRequirements": "Password must be at least 10 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character.", + "utils.validation.passwordsDoNotMatch": "Passwords do not match.", + "utils.validation.required": "This field is required" } diff --git a/packages/utils/locales/es.json b/packages/utils/locales/es.json index 78791e197..af6ed987f 100644 --- a/packages/utils/locales/es.json +++ b/packages/utils/locales/es.json @@ -1,39 +1,43 @@ { - "utils.common.loading": "Cargando...", - "utils.common.error": "Error", - "utils.common.success": "Éxito", + "utils.common.back": "Atrás", "utils.common.cancel": "Cancelar", + "utils.common.close": "Cerrar", "utils.common.confirm": "Confirmar", - "utils.common.save": "Guardar", "utils.common.delete": "Eliminar", "utils.common.edit": "Editar", - "utils.common.close": "Cerrar", - "utils.common.back": "Atrás", + "utils.common.error": "Error", + "utils.common.filter": "Filtrar", + "utils.common.loading": "Cargando...", "utils.common.next": "Siguiente", "utils.common.previous": "Anterior", - "utils.common.submit": "Enviar", + "utils.common.save": "Guardar", "utils.common.search": "Buscar", - "utils.common.filter": "Filtrar", "utils.common.sort": "Ordenar", - "utils.common.viewMore": "Ver Más", + "utils.common.submit": "Enviar", + "utils.common.success": "Éxito", "utils.common.viewLess": "Ver Menos", - "utils.date.today": "Hoy", - "utils.date.yesterday": "Ayer", - "utils.date.period": "Período", - "utils.date.startDate": "Fecha de inicio", + "utils.common.viewMore": "Ver Más", "utils.date.endDate": "Fecha de fin", "utils.date.from": "Desde {date}", - "utils.date.until": "Hasta {date}", + "utils.date.period": "Período", "utils.date.range": "{from} - {to}", - "utils.time.minutesAgo": "{count, plural, one {hace 1 minuto} other {hace {count} minutos}}", - "utils.time.hoursAgo": "{count, plural, one {hace 1 hora} other {hace {count} horas}}", + "utils.date.startDate": "Fecha de inicio", + "utils.date.today": "Hoy", + "utils.date.until": "Hasta {date}", + "utils.date.yesterday": "Ayer", "utils.time.daysAgo": "{count, plural, one {hace 1 día} other {hace {count} días}}", - "utils.time.weeksAgo": "{count, plural, one {hace 1 semana} other {hace {count} semanas}}", + "utils.time.hoursAgo": "{count, plural, one {hace 1 hora} other {hace {count} horas}}", + "utils.time.minutesAgo": "{count, plural, one {hace 1 minuto} other {hace {count} minutos}}", "utils.time.monthsAgo": "{count, plural, one {hace 1 mes} other {hace {count} meses}}", - "utils.validation.required": "Este campo es obligatorio", + "utils.time.weeksAgo": "{count, plural, one {hace 1 semana} other {hace {count} semanas}}", "utils.validation.email": "Por favor ingrese un correo electrónico válido", - "utils.validation.minLength": "La longitud mínima es de {min} caracteres", - "utils.validation.maxLength": "La longitud máxima es de {max} caracteres", + "utils.validation.emailFormat": "Ingresa una dirección de correo electrónico con un formato válido.", + "utils.validation.fieldRequired": "Este campo es obligatorio.", "utils.validation.invalidDate": "Su fecha no es válida", - "utils.validation.minDate": "La fecha de fin no puede ser anterior a la fecha de inicio" + "utils.validation.maxLength": "La longitud máxima es de {max} caracteres", + "utils.validation.minDate": "La fecha de fin no puede ser anterior a la fecha de inicio", + "utils.validation.minLength": "La longitud mínima es de {min} caracteres", + "utils.validation.passwordRequirements": "La contraseña debe tener al menos 10 caracteres y contener al menos una letra mayúscula, una letra minúscula, un número y un carácter especial.", + "utils.validation.passwordsDoNotMatch": "Las contraseñas no coinciden.", + "utils.validation.required": "Este campo es obligatorio" } diff --git a/packages/utils/locales/pt.json b/packages/utils/locales/pt.json index 9e5a1015b..417f797fb 100644 --- a/packages/utils/locales/pt.json +++ b/packages/utils/locales/pt.json @@ -1,39 +1,43 @@ { - "utils.common.loading": "Carregando...", - "utils.common.error": "Erro", - "utils.common.success": "Sucesso", + "utils.common.back": "Voltar", "utils.common.cancel": "Cancelar", + "utils.common.close": "Fechar", "utils.common.confirm": "Confirmar", - "utils.common.save": "Salvar", "utils.common.delete": "Excluir", "utils.common.edit": "Editar", - "utils.common.close": "Fechar", - "utils.common.back": "Voltar", + "utils.common.error": "Erro", + "utils.common.filter": "Filtrar", + "utils.common.loading": "Carregando...", "utils.common.next": "Próximo", "utils.common.previous": "Anterior", - "utils.common.submit": "Enviar", + "utils.common.save": "Salvar", "utils.common.search": "Buscar", - "utils.common.filter": "Filtrar", "utils.common.sort": "Ordenar", - "utils.common.viewMore": "Ver Mais", + "utils.common.submit": "Enviar", + "utils.common.success": "Sucesso", "utils.common.viewLess": "Ver Menos", - "utils.date.today": "Hoje", - "utils.date.yesterday": "Ontem", - "utils.date.period": "Período", - "utils.date.startDate": "Data de início", + "utils.common.viewMore": "Ver Mais", "utils.date.endDate": "Data de término", "utils.date.from": "De {date}", - "utils.date.until": "Até {date}", + "utils.date.period": "Período", "utils.date.range": "{from} - {to}", - "utils.time.minutesAgo": "{count, plural, one {há 1 minuto} other {há {count} minutos}}", - "utils.time.hoursAgo": "{count, plural, one {há 1 hora} other {há {count} horas}}", + "utils.date.startDate": "Data de início", + "utils.date.today": "Hoje", + "utils.date.until": "Até {date}", + "utils.date.yesterday": "Ontem", "utils.time.daysAgo": "{count, plural, one {há 1 dia} other {há {count} dias}}", - "utils.time.weeksAgo": "{count, plural, one {há 1 semana} other {há {count} semanas}}", + "utils.time.hoursAgo": "{count, plural, one {há 1 hora} other {há {count} horas}}", + "utils.time.minutesAgo": "{count, plural, one {há 1 minuto} other {há {count} minutos}}", "utils.time.monthsAgo": "{count, plural, one {há 1 mês} other {há {count} meses}}", - "utils.validation.required": "Este campo é obrigatório", + "utils.time.weeksAgo": "{count, plural, one {há 1 semana} other {há {count} semanas}}", "utils.validation.email": "Por favor, insira um endereço de e-mail válido", - "utils.validation.minLength": "O comprimento mínimo é de {min} caracteres", - "utils.validation.maxLength": "O comprimento máximo é de {max} caracteres", + "utils.validation.emailFormat": "Informe um endereço de e-mail em um formato válido.", + "utils.validation.fieldRequired": "Este campo é obrigatório.", "utils.validation.invalidDate": "Sua data não é válida", - "utils.validation.minDate": "A data de término não pode ser anterior à data de início" + "utils.validation.maxLength": "O comprimento máximo é de {max} caracteres", + "utils.validation.minDate": "A data de término não pode ser anterior à data de início", + "utils.validation.minLength": "O comprimento mínimo é de {min} caracteres", + "utils.validation.passwordRequirements": "A senha deve ter pelo menos 10 caracteres e conter pelo menos uma letra maiúscula, uma letra minúscula, um número e um caractere especial.", + "utils.validation.passwordsDoNotMatch": "As senhas não coincidem.", + "utils.validation.required": "Este campo é obrigatório" } diff --git a/packages/utils/package.json b/packages/utils/package.json index 6321a4db7..cbca73f9a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -42,7 +42,7 @@ "@baseapp-frontend/eslint-plugin": "workspace:*", "@baseapp-frontend/test": "workspace:*", "@baseapp-frontend/tsconfig": "workspace:*", - "@formatjs/cli": "^6.7.4", + "@formatjs/cli": "catalog:", "@testing-library/jest-dom": "catalog:test", "@testing-library/react": "catalog:test", "@testing-library/user-event": "catalog:test", diff --git a/packages/utils/types/intl.ts b/packages/utils/types/intl.ts new file mode 100644 index 000000000..11ca65937 --- /dev/null +++ b/packages/utils/types/intl.ts @@ -0,0 +1,3 @@ +import type { IntlShape } from '@formatjs/intl' + +export type IntlFormatter = Pick diff --git a/packages/utils/types/zod.ts b/packages/utils/types/zod.ts new file mode 100644 index 000000000..7ef769310 --- /dev/null +++ b/packages/utils/types/zod.ts @@ -0,0 +1,3 @@ +import type { ZOD_MESSAGE } from '../constants/zod' + +export type ZodMessages = Record diff --git a/packages/wagtail/locales/en.json b/packages/wagtail/locales/en.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/packages/wagtail/locales/en.json @@ -0,0 +1 @@ +{} diff --git a/packages/wagtail/locales/es.json b/packages/wagtail/locales/es.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/packages/wagtail/locales/es.json @@ -0,0 +1 @@ +{} diff --git a/packages/wagtail/locales/index.ts b/packages/wagtail/locales/index.ts new file mode 100644 index 000000000..78ecf303c --- /dev/null +++ b/packages/wagtail/locales/index.ts @@ -0,0 +1,7 @@ +/** + * Export all locale files for dynamic loading + * This allows the locale loader to automatically import them + */ +export { default as en } from './en.json' +export { default as es } from './es.json' +export { default as pt } from './pt.json' diff --git a/packages/wagtail/locales/pt.json b/packages/wagtail/locales/pt.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/packages/wagtail/locales/pt.json @@ -0,0 +1 @@ +{} diff --git a/packages/wagtail/package.json b/packages/wagtail/package.json index f9ca21b08..8abada528 100644 --- a/packages/wagtail/package.json +++ b/packages/wagtail/package.json @@ -23,6 +23,7 @@ "axios": "catalog:", "graphql": "catalog:graphql", "next": "catalog:", + "react-intl": "catalog:", "react-relay": "catalog:graphql" }, "peerDependencies": { diff --git a/packages/wagtail/tsconfig.json b/packages/wagtail/tsconfig.json index 1238b4074..2efefea58 100644 --- a/packages/wagtail/tsconfig.json +++ b/packages/wagtail/tsconfig.json @@ -8,6 +8,7 @@ "**/*.ts", "**/*.tsx", "**/*.d.ts", + "locales/*.json", ".eslintrc.js", "*.ts", "*.tsx", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ef92252c..03e9a5413 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ catalogs: specifier: ^3.2.0 version: 3.3.0 default: + '@formatjs/cli': + specifier: 6.16.33 + version: 6.16.33 '@formatjs/intl': specifier: 2.10.10 version: 2.10.10 @@ -824,7 +827,7 @@ importers: specifier: catalog:test version: 9.9.0 '@formatjs/cli': - specifier: ^6.7.4 + specifier: 'catalog:' version: 6.16.33 '@playwright/test': specifier: 1.62.0 @@ -1138,6 +1141,9 @@ importers: react-international-phone: specifier: 'catalog:' version: 4.5.0(react@19.2.0) + react-intl: + specifier: 'catalog:' + version: 7.1.14(react@19.2.0)(typescript@5.9.2) react-lazy-load-image-component: specifier: catalog:design-system version: 1.6.3(react@19.2.0) @@ -1427,7 +1433,7 @@ importers: specifier: workspace:* version: link:../tsconfig '@formatjs/cli': - specifier: ^6.7.4 + specifier: 'catalog:' version: 6.16.33 '@types/luxon': specifier: 'catalog:' @@ -1645,7 +1651,7 @@ importers: specifier: workspace:* version: link:../tsconfig '@formatjs/cli': - specifier: ^6.7.4 + specifier: 'catalog:' version: 6.16.33 '@testing-library/jest-dom': specifier: catalog:test @@ -1731,6 +1737,9 @@ importers: react: specifier: catalog:react19 version: 19.2.0 + react-intl: + specifier: 'catalog:' + version: 7.1.14(react@19.2.0)(typescript@5.9.2) react-relay: specifier: catalog:graphql version: 19.0.0(react@19.2.0) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0fcb9045a..f009d50a2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -23,6 +23,7 @@ settings: trustPolicy: no-downgrade catalog: + "@formatjs/cli": 6.16.33 "@formatjs/intl": 2.10.10 "@mdxeditor/editor": 3.54.0 "@mdxeditor/gurx": 1.2.4