From b56f3c2c7626229e0c6f40e22d444244c4130de0 Mon Sep 17 00:00:00 2001 From: Alisson Patricio Date: Fri, 9 Oct 2026 02:12:07 +0000 Subject: [PATCH 1/5] i18n: load package locales per locale and drop native-unsafe intl from shared comment hook Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FAfWR6vcsVFP1SxjTfyqD1 --- packages/components/locales/en.json | 1 + packages/components/locales/es.json | 13 +++---- packages/components/locales/pt.json | 13 +++---- .../common/hooks/useCommentActions/index.ts | 24 ++----------- .../useCommentOptions/constants.ts | 24 +++++++++++++ .../CommentItem/useCommentOptions/index.tsx | 17 +++++++-- packages/components/package.json | 2 +- packages/design-system/locales/en.json | 1 + packages/design-system/locales/es.json | 1 + packages/design-system/locales/index.ts | 7 ++++ packages/design-system/locales/pt.json | 1 + packages/design-system/package.json | 3 ++ .../withDesignSystemTestProviders/index.tsx | 36 ++++++++++--------- packages/design-system/tsconfig.json | 11 +++++- packages/i18n/locales/en.json | 19 +--------- packages/i18n/locales/es.json | 19 +--------- packages/i18n/locales/pt.json | 19 +--------- packages/i18n/package.json | 2 +- .../i18n/providers/IntlProvider/index.tsx | 19 ++++++---- packages/i18n/providers/IntlProvider/types.ts | 1 + packages/utils/package.json | 2 +- packages/wagtail/locales/en.json | 1 + packages/wagtail/locales/es.json | 1 + packages/wagtail/locales/index.ts | 7 ++++ packages/wagtail/locales/pt.json | 1 + packages/wagtail/package.json | 1 + packages/wagtail/tsconfig.json | 1 + pnpm-lock.yaml | 15 ++++++-- pnpm-workspace.yaml | 1 + 29 files changed, 144 insertions(+), 119 deletions(-) create mode 100644 packages/components/modules/comments/web/CommentItem/useCommentOptions/constants.ts create mode 100644 packages/design-system/locales/en.json create mode 100644 packages/design-system/locales/es.json create mode 100644 packages/design-system/locales/index.ts create mode 100644 packages/design-system/locales/pt.json create mode 100644 packages/wagtail/locales/en.json create mode 100644 packages/wagtail/locales/es.json create mode 100644 packages/wagtail/locales/index.ts create mode 100644 packages/wagtail/locales/pt.json diff --git a/packages/components/locales/en.json b/packages/components/locales/en.json index e43d9079d..ef223df7e 100644 --- a/packages/components/locales/en.json +++ b/packages/components/locales/en.json @@ -1,4 +1,5 @@ { + "comments.actions.delete": "Delete Comment", "comments.actions.edit": "Edit Comment", "comments.actions.pin": "Pin Comment", "comments.actions.share": "Share Comment", diff --git a/packages/components/locales/es.json b/packages/components/locales/es.json index e0b5e2d35..1ec47af6c 100644 --- a/packages/components/locales/es.json +++ b/packages/components/locales/es.json @@ -1,12 +1,13 @@ { - "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.loading.more": "cargando más comentarios", - "comments.reply.ariaLabel": "responder al comentario {commentId}", - "comments.reply.count.ariaLabel": "cantidad de respuestas {commentId}", + "comments.pinned.badge": "Fijado", "comments.replies.showMore": "Mostrar más respuestas ({count})", - "comments.pinned.badge": "Fijado" + "comments.reply.ariaLabel": "responder al comentario {commentId}", + "comments.reply.count.ariaLabel": "cantidad de respuestas {commentId}" } diff --git a/packages/components/locales/pt.json b/packages/components/locales/pt.json index b6c1c5bca..73de7836d 100644 --- a/packages/components/locales/pt.json +++ b/packages/components/locales/pt.json @@ -1,12 +1,13 @@ { - "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.loading.more": "carregando mais comentários", - "comments.reply.ariaLabel": "responder ao comentário {commentId}", - "comments.reply.count.ariaLabel": "contagem de respostas {commentId}", + "comments.pinned.badge": "Fixado", "comments.replies.showMore": "Mostrar mais respostas ({count})", - "comments.pinned.badge": "Fixado" + "comments.reply.ariaLabel": "responder ao comentário {commentId}", + "comments.reply.count.ariaLabel": "contagem de respostas {commentId}" } 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/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/package.json b/packages/components/package.json index 49c6dcbfe..b392a14f5 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/locales/en.json b/packages/design-system/locales/en.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/packages/design-system/locales/en.json @@ -0,0 +1 @@ +{} diff --git a/packages/design-system/locales/es.json b/packages/design-system/locales/es.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/packages/design-system/locales/es.json @@ -0,0 +1 @@ +{} 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..0967ef424 --- /dev/null +++ b/packages/design-system/locales/pt.json @@ -0,0 +1 @@ +{} 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..45850c24b 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,30 @@ 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, setMessages] = useState>(() => 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)) + setMessages(getLocaleMessages(currentLocale)) }) } - }, [initialLocale, initialCookies, additionalMessages]) + }, [initialLocale, initialCookies, getLocaleMessages]) useEffect(() => { if (initialLocale && initialLocale !== locale) { setLocale(initialLocale) - setMessages(getMessages(initialLocale, additionalMessages)) + setMessages(getLocaleMessages(initialLocale)) } - }, [initialLocale, locale, additionalMessages]) + }, [initialLocale, locale, getLocaleMessages]) 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/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/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 From 30fa346c69724d7328ecd376ca714c016f88cd3b Mon Sep 17 00:00:00 2001 From: Alisson Patricio Date: Fri, 9 Oct 2026 02:38:40 +0000 Subject: [PATCH 2/5] i18n: translate all web strings to en/es/pt Route every user-facing string in components, design-system, utils and authentication web code through react-intl, with es/pt catalogs. Native and shared code used by native keep their English strings. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FAfWR6vcsVFP1SxjTfyqD1 --- .changeset/i18n-web-translations.md | 23 + .../access/useChangePassword/constants.ts | 38 +- .../modules/access/useLogin/constants.ts | 21 +- .../modules/access/useLogin/index.ts | 5 +- .../modules/access/useLogin/types.ts | 7 +- .../access/useRecoverPassword/constants.ts | 19 +- .../access/useRequestEmailChange/constants.ts | 19 +- .../access/useResetPassword/constants.ts | 36 +- .../modules/access/useSignUp/constants.ts | 56 ++- .../authentication/modules/mfa/constants.ts | 19 +- packages/components/locales/en.json | 407 +++++++++++++++++- packages/components/locales/es.json | 407 +++++++++++++++++- packages/components/locales/pt.json | 407 +++++++++++++++++- .../DefaultHoverOverlay/index.tsx | 74 ++-- .../ThreeDotsMenuHoverOverlay/index.tsx | 98 +++-- .../__shared__/web/ActionsOverlay/index.tsx | 35 +- .../web/SearchNotFoundState/index.tsx | 14 +- .../web/SocialInput/SubmitActions/index.tsx | 29 +- .../__shared__/web/SocialInput/index.tsx | 13 +- .../__shared__/web/Timestamp/index.tsx | 7 +- .../web/UpdateSubmitActions/index.tsx | 50 ++- .../DateFilterChip/index.tsx | 18 +- .../DateFilterComponent/index.tsx | 31 +- .../EventFilterChip/index.tsx | 13 +- .../ActivityLogComponent/LogGroups/index.tsx | 13 +- .../ActivityLogComponent/LogItem/constants.ts | 28 ++ .../ActivityLogComponent/LogItem/index.tsx | 17 +- .../web/ActivityLogComponent/constants.ts | 40 ++ .../web/ActivityLogComponent/index.tsx | 16 +- .../web/ActivityLogComponent/utils.ts | 20 +- .../CommentFilesUpsertActions/index.tsx | 16 +- .../comments/web/CommentCreate/index.tsx | 5 +- .../CommentReactionButton/index.tsx | 61 ++- .../comments/web/CommentItem/index.tsx | 22 +- .../CommentFilesUpsertActions/index.tsx | 44 +- .../content-feed/web/ContentFeed/index.tsx | 6 +- .../content-feed/web/PostCreate/index.tsx | 18 +- .../content-feed/web/PostFooter/index.tsx | 20 +- .../content-feed/web/PostForm/index.tsx | 21 +- .../content-feed/web/PostHeader/index.tsx | 10 +- .../web/PostImageDropzone/index.tsx | 6 +- .../content-feed/web/PostImageSlide/index.tsx | 13 +- .../content-feed/web/PostList/index.tsx | 7 +- .../web/PostReactionButton/index.tsx | 61 ++- .../files/web/AttachedFileItem/index.tsx | 60 ++- .../modules/files/web/FileThumbnail/index.tsx | 4 +- .../files/web/FileUploadDropzone/index.tsx | 13 +- .../UploadingFileItem/FileProgress.tsx | 17 +- .../UploadingFileItem/index.tsx | 27 +- .../files/web/FileUploadList/index.tsx | 12 +- .../files/web/FileUploadTrigger/index.tsx | 7 +- .../messages/common/useLeaveGroup/index.tsx | 36 +- .../messages/common/useLeaveGroup/types.ts | 1 + .../modules/messages/common/utils.ts | 2 + .../GroupsList/GroupItem/index.tsx | 22 +- .../GroupsList/index.tsx | 50 ++- .../web/AddContactToGroupDialog/index.tsx | 12 +- .../ChatRoomItem/constants.ts | 28 ++ .../AllChatRoomsList/ChatRoomItem/index.tsx | 15 +- .../AllChatRoomsList/ChatRoomItem/utils.ts | 21 +- .../EmptyChatRoomsState/index.tsx | 6 +- .../web/AllChatRoomsList/constants.ts | 17 +- .../messages/web/AllChatRoomsList/index.tsx | 13 +- .../messages/web/ChatCreate/Body/index.tsx | 52 ++- .../web/ChatCreate/ChatRoomListItem/index.tsx | 3 +- .../messages/web/ChatCreate/Header/index.tsx | 61 ++- .../modules/messages/web/ChatCreate/index.tsx | 5 +- .../ChatRoomHeader/ChatRoomOptions/index.tsx | 21 +- .../web/ChatRoom/ChatRoomHeader/index.tsx | 30 +- .../modules/messages/web/ChatRoom/index.tsx | 7 +- .../web/GroupChatCreate/Header/index.tsx | 50 ++- .../messages/web/GroupChatCreate/index.tsx | 7 +- .../web/GroupChatDetails/Body/index.tsx | 17 +- .../web/GroupChatDetails/Header/index.tsx | 50 ++- .../ProfileCard/AdminOptionsMenu/index.tsx | 23 +- .../ProfileCard/MemberOptionsMenu/index.tsx | 8 +- .../RemoveAdminPermissionsDialog/index.tsx | 51 ++- .../GroupChatDetails/ProfileCard/index.tsx | 18 +- .../messages/web/GroupChatDetails/index.tsx | 5 +- .../web/GroupChatEdit/AddMemberCard/index.tsx | 7 +- .../AddMembersDialog/constants.ts | 8 + .../GroupChatEdit/AddMembersDialog/index.tsx | 17 +- .../AddMembersMobile/constants.ts | 8 + .../GroupChatEdit/AddMembersMobile/index.tsx | 22 +- .../web/GroupChatEdit/Header/index.tsx | 60 ++- .../messages/web/GroupChatEdit/constants.ts | 8 + .../messages/web/GroupChatEdit/index.tsx | 10 +- .../messages/web/MessageUpdate/index.tsx | 12 +- .../UserMessage/MessageItem/constants.ts | 42 ++ .../UserMessage/MessageItem/index.tsx | 29 +- .../MessagesGroup/UserMessage/index.tsx | 7 +- .../MessagesList/MessagesGroup/constants.ts | 12 + .../web/MessagesList/MessagesGroup/index.tsx | 11 +- .../web/MessagesList/MessagesGroup/utils.ts | 11 +- .../messages/web/MessagesList/index.tsx | 7 +- .../web/ProfileSummary/Body/index.tsx | 29 +- .../web/ProfileSummary/Header/index.tsx | 34 +- .../__tests__/SendMessage.test.tsx | 10 +- .../messages/web/SendMessage/index.tsx | 18 +- .../EditGroupTitleAndImage/index.tsx | 22 +- .../EmptyProfilesListState/index.tsx | 11 +- .../ProfileCard/index.tsx | 8 +- .../ProfilesList/index.tsx | 34 +- .../__shared__/GroupChatMembersList/index.tsx | 10 +- .../__shared__/LeaveGroupDialog/constants.ts | 61 +++ .../web/__shared__/LeaveGroupDialog/index.tsx | 34 +- .../messages/web/__shared__/constants.ts | 93 ++++ .../modules/messages/web/__shared__/utils.ts | 11 + .../AccountPopover/LogoutItem/index.tsx | 13 +- .../AccountMenu/AccountPopover/index.tsx | 8 +- .../AccountMenu/VerticalFooter/index.tsx | 7 +- .../web/Header/AccountMenu/index.tsx | 12 +- .../NotificationsList/EmptyState/index.tsx | 13 +- .../NotificationsList/LoadingState/index.tsx | 6 +- .../MarkAllAsReadButton/index.tsx | 3 +- .../NotificationItem/CommentCreated/index.tsx | 14 +- .../NotificationItem/CommentReply/index.tsx | 9 +- .../Notification/NotificationAvatar/index.tsx | 28 +- .../ReactionCreated/index.tsx | 14 +- .../web/NotificationsList/constants.ts | 8 + .../web/NotificationsList/index.tsx | 19 +- .../web/NotificationsPopover/constants.ts | 12 + .../web/NotificationsPopover/index.tsx | 11 +- .../modules/pages/web/PageComponent/index.tsx | 5 +- .../payments/web/AddCardModal/index.tsx | 76 +++- .../SubscriptionCard/index.tsx | 42 +- .../web/AvailableSubscriptions/index.tsx | 9 +- .../ConfirmationSubscriptionModal/index.tsx | 28 +- .../web/CheckoutComponent/constants.ts | 21 + .../payments/web/CheckoutComponent/index.tsx | 73 +++- .../payments/web/CheckoutComponent/utils.ts | 7 +- .../InvoiceItemTableRow/index.tsx | 9 +- .../InvoiceItemWrapper/index.tsx | 19 +- .../InvoiceListTableHeader/index.tsx | 30 +- .../MobileInvoiceItemTableRow/index.tsx | 5 +- .../web/InvoiceListTable/constants.ts | 43 ++ .../payments/web/InvoiceListTable/index.tsx | 13 +- .../payments/web/InvoiceListTable/types.ts | 1 + .../AddPaymentMethodItem/index.tsx | 12 +- .../PaymentMethodDisplay/index.tsx | 15 +- .../PaymentMethodsItem/index.tsx | 35 +- .../index.tsx | 87 +++- .../CancelSubscriptionModal/index.tsx | 15 +- .../FreePlanComponent/index.tsx | 25 +- .../web/SubscriptionManagement/constants.ts | 43 ++ .../web/SubscriptionManagement/index.tsx | 74 +++- .../modules/payments/web/constants.ts | 53 +++ .../payments/web/hooks/useStripeHook.tsx | 37 +- .../graphql/mutations/CancelInvitation.ts | 18 +- .../mutations/ProfileUserRoleCreate.ts | 13 +- .../common/graphql/mutations/RemoveMember.ts | 10 +- .../common/graphql/mutations/ReportCreate.ts | 10 +- .../graphql/mutations/ResendInvitation.ts | 18 +- .../graphql/mutations/SendInvitation.ts | 13 +- .../profiles/web/PermissionDenied/index.tsx | 14 +- .../BlockButtonWithDialog/constants.ts | 52 ++- .../BlockButtonWithDialog/index.tsx | 35 +- .../FollowToggleButton/index.tsx | 7 +- .../components/ConfirmationStep.tsx | 15 +- .../components/ReportDialogContent.tsx | 11 +- .../components/SelectReportTypeStep.tsx | 8 +- .../components/SummaryStep.tsx | 31 +- .../components/TextStep.tsx | 19 +- .../ReportButtonWithDialog/constants.ts | 18 + .../ReportButtonWithDialog/index.tsx | 3 +- .../profiles/web/ProfileComponent/index.tsx | 23 +- .../InviteMemberDialog/MemberSearch/index.tsx | 8 +- .../InviteMemberDialog/constants.ts | 47 +- .../InviteMemberDialog/index.tsx | 37 +- .../web/ProfileMembers/MemberItem/index.tsx | 73 +++- .../web/ProfileMembers/MembersList/index.tsx | 9 +- .../profiles/web/ProfileMembers/constants.ts | 75 +++- .../profiles/web/ProfileMembers/index.tsx | 33 +- .../web/ProfileSettingsComponent/constants.ts | 34 ++ .../web/ProfileSettingsComponent/index.tsx | 96 ++++- .../CreateProfileModal/constants.ts | 64 +++ .../CreateProfileModal/index.tsx | 65 ++- .../AddProfileMenuItem/index.tsx | 10 +- .../profile-popover/CurrentProfile/index.tsx | 7 +- .../ProfilesList/EmptyState/index.tsx | 13 +- .../ProfilesList/ProfileMenuItem/index.tsx | 19 +- .../profile-popover/ProfilesList/index.tsx | 23 +- .../SwitchProfileMenu/index.tsx | 7 +- packages/design-system/.storybook/preview.ts | 2 +- .../avatars/AvatarWithPlaceholder/index.tsx | 54 ++- .../web/avatars/ClickableAvatar/index.tsx | 11 +- .../web/buttons/FileUploadButton/index.tsx | 21 +- .../web/dialogs/ConfirmDialog/index.tsx | 67 +-- .../Dropzone/DropzonePreview/index.tsx | 16 +- .../web/dropzones/Dropzone/index.tsx | 54 ++- .../web/dropzones/Dropzone/utils.tsx | 5 + .../web/inputs/AutocompleteField/index.tsx | 77 ++-- .../InitializedMDXEditor/index.tsx | 26 +- .../mentions/MentionsMenuPortal/index.tsx | 10 +- .../inputs/MarkdownEditorField/constants.ts | 254 +++++++++++ .../components/web/inputs/Searchbar/index.tsx | 106 ++--- .../web/inputs/SocialTextField/index.tsx | 31 +- .../web/inputs/SocialTextField/utils.tsx | 9 + .../web/logos/CustomLogoCondensed/index.tsx | 7 +- packages/design-system/locales/en.json | 88 +++- packages/design-system/locales/es.json | 88 +++- packages/design-system/locales/pt.json | 88 +++- packages/utils/constants/zod.ts | 22 + .../functions/zod/getZodMessages/index.ts | 10 + packages/utils/functions/zod/index.ts | 1 + packages/utils/index.ts | 3 + packages/utils/locales/en.json | 46 +- packages/utils/locales/es.json | 46 +- packages/utils/locales/pt.json | 46 +- packages/utils/types/intl.ts | 3 + packages/utils/types/zod.ts | 3 + 211 files changed, 5598 insertions(+), 1195 deletions(-) create mode 100644 .changeset/i18n-web-translations.md create mode 100644 packages/components/modules/activity-log/web/ActivityLogComponent/LogItem/constants.ts create mode 100644 packages/components/modules/messages/web/AllChatRoomsList/ChatRoomItem/constants.ts create mode 100644 packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/constants.ts create mode 100644 packages/components/modules/messages/web/MessagesList/MessagesGroup/constants.ts create mode 100644 packages/components/modules/messages/web/__shared__/LeaveGroupDialog/constants.ts create mode 100644 packages/components/modules/messages/web/__shared__/constants.ts create mode 100644 packages/components/modules/messages/web/__shared__/utils.ts create mode 100644 packages/components/modules/notifications/web/NotificationsList/constants.ts create mode 100644 packages/components/modules/notifications/web/NotificationsPopover/constants.ts create mode 100644 packages/components/modules/payments/web/InvoiceListTable/constants.ts create mode 100644 packages/components/modules/payments/web/SubscriptionManagement/constants.ts create mode 100644 packages/components/modules/profiles/web/ProfileSettingsComponent/constants.ts create mode 100644 packages/design-system/components/web/dropzones/Dropzone/utils.tsx create mode 100644 packages/design-system/components/web/inputs/SocialTextField/utils.tsx create mode 100644 packages/utils/functions/zod/getZodMessages/index.ts create mode 100644 packages/utils/functions/zod/index.ts create mode 100644 packages/utils/types/intl.ts create mode 100644 packages/utils/types/zod.ts 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 ef223df7e..11efccf02 100644 --- a/packages/components/locales/en.json +++ b/packages/components/locales/en.json @@ -1,13 +1,418 @@ { + "activityLog.dateFilter.apply": "Filter", + "activityLog.dateFilter.clear": "Clear Filter", + "activityLog.dateFilter.endDate": "End date", + "activityLog.dateFilter.error.invalidDate": "Your date is not valid", + "activityLog.dateFilter.error.minDate": "End date cannot be earlier than start date.", + "activityLog.dateFilter.from": "From {date}", + "activityLog.dateFilter.notFound": "No results found for the selected date range.", + "activityLog.dateFilter.period": "Period", + "activityLog.dateFilter.startDate": "Start date", + "activityLog.dateFilter.until": "Until {date}", + "activityLog.eventFilter.label": "Filter", + "activityLog.eventFilter.option.all": "All", + "activityLog.eventFilter.option.comments": "Comments", + "activityLog.eventFilter.option.posts": "Posts", + "activityLog.eventFilter.option.reactions": "Reactions", + "activityLog.loading.more": "loading more activity logs", + "activityLog.logGroups.deletedUser": "Deleted User", + "activityLog.logItem.updated": "Updated {name}", + "activityLog.logItem.updatedBio": "Updated their bio", + "activityLog.logItem.updatedProfileBanner": "Updated their profile banner", + "activityLog.logItem.updatedProfilePicture": "Updated their profile picture", + "activityLog.logItem.verb.addComment": "Created a comment", + "activityLog.logItem.verb.addReaction": "Added a reaction", + "activityLog.logItem.verb.changeComment": "Edited a comment", + "activityLog.logItem.verb.deleteComment": "Deleted a comment", + "activityLog.logItem.verb.pinComment": "Pinned a comment", + "activityLog.logItem.verb.replyComment": "Replied to a comment", + "activityLog.search.placeholder": "Search by user", + "activityLog.title": "Activity Log", "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", + "contentFeed.loading.more": "loading more posts", + "contentFeed.newPost": "New Post", + "contentFeed.postCreate.error": "Something went wrong", + "contentFeed.postCreate.success": "Post Created Successfully", + "contentFeed.postFooter.reply.ariaLabel": "Reply", + "contentFeed.postFooter.share.ariaLabel": "Share", + "contentFeed.postForm.content.placeholder": "What is on your mind?", + "contentFeed.postForm.disableReactions": "Disable Reactions to this post", + "contentFeed.postForm.publish": "Publish", + "contentFeed.postForm.title": "New Post", + "contentFeed.postHeader.options.ariaLabel": "Post Options", + "contentFeed.postImageDropzone.title": "Click to browse or drag and drop images and videos.", + "contentFeed.postImageSlide.alt": "Post Image", + "contentFeed.reaction.ariaLabel": "react to comment {postId}", + "contentFeed.reaction.count.ariaLabel": "reactions count {postId}", + "contentFeed.title": "Content Feed", + "files.actions.download": "Download", + "files.actions.pause": "Pause", + "files.actions.remove": "Remove", + "files.actions.resume": "Resume", + "files.actions.retry": "Retry", + "files.attachedFileItem.status": "Status: {status}", + "files.attachedFileItem.uploadedBy": "Uploaded by {name}", + "files.dropzone.subtitle": "Max {maxFiles} files, {maxFileSize}MB each", + "files.dropzone.title": "Upload Files", + "files.preview.alt": "File preview", + "files.progress.aborted": "Aborted", + "files.progress.completed": "Completed", + "files.progress.failed": "Failed", + "files.progress.paused": "Paused", + "files.progress.pending": "Pending...", + "files.progress.uploading": "Uploading {progress}%", + "files.uploadList.attached": "Attached Files ({count})", + "files.uploadList.uploading": "Uploading ({count})", + "files.uploadTrigger.label": "Attach files", + "messages.addContactToGroup.groupItem.alreadyAdded": " • Already added", + "messages.addContactToGroup.groupItem.selectAriaLabel": "select group {title}", + "messages.addContactToGroup.groupsList.empty.description": "You can only add contacts to groups you manage.", + "messages.addContactToGroup.groupsList.empty.title": "No groups yet", + "messages.addContactToGroup.groupsList.loading": "loading groups", + "messages.addContactToGroup.groupsList.loadingMore": "loading more groups", + "messages.addContactToGroup.submit": "Add to Group", + "messages.addContactToGroup.title": "Add contact to a group", + "messages.chatCreate.header.title": "New Chat", + "messages.chatCreate.item.message": "Message", + "messages.chatCreate.newGroupButton": "New Group", + "messages.chatRoom.header.backAriaLabel": "return to chat room list", + "messages.chatRoom.header.optionsAriaLabel": "Show chatroom options", + "messages.chatRoom.notFound": "Chat room not found", + "messages.chatRoom.options.groupDetails": "Group Details", + "messages.chatRoomItem.date.daysAgo": "{count, plural, one {# day ago} other {# days ago}}", + "messages.chatRoomItem.date.monthsAgo": "{count, plural, one {# month ago} other {# months ago}}", + "messages.chatRoomItem.date.weeksAgo": "{count, plural, one {# week ago} other {# weeks ago}}", + "messages.chatRoomItem.deleteDialog.title": "Delete Chat?", + "messages.chatRoomItem.itemTitle": "Chat", + "messages.chatRoomItem.markAsUnread": "Mark as Unread", + "messages.chatRoomsList.empty": "No messages to be displayed.", + "messages.chatRoomsList.loadingMore": "loading more rooms", + "messages.chatRoomsList.new": "New", + "messages.chatRoomsList.tabs.active": "Active", + "messages.chatRoomsList.tabs.archived": "Archived", + "messages.chatRoomsList.tabs.groups": "Groups", + "messages.chatRoomsList.tabs.unread": "Unread", + "messages.chatRoomsList.title": "Messages", + "messages.editGroupTitleAndImage.changeAvatar": "Change Avatar", + "messages.editGroupTitleAndImage.groupName": "Group Name", + "messages.editGroupTitleAndImage.uploadAvatar": "Upload Avatar", + "messages.emptyProfilesList.description": "Once you connect to other profiles they will be shown here.", + "messages.emptyProfilesList.title": "Search for profiles", + "messages.groupChatCreate.header.cancelAriaLabel": "cancel group creation", + "messages.groupChatCreate.header.createAriaLabel": "Create group", + "messages.groupChatCreate.header.title": "New Group", + "messages.groupChatDetails.adminOptions.promoteToAdmin": "Promote to admin", + "messages.groupChatDetails.adminOptions.removeAdmin": "Remove admin permissions", + "messages.groupChatDetails.adminOptions.removeFromGroup": "Remove from group", + "messages.groupChatDetails.header.editAriaLabel": "edit group chat", + "messages.groupChatDetails.header.title": "Group View", + "messages.groupChatDetails.membersAriaLabel": "group members", + "messages.groupChatDetails.profileCard.admin": "Admin", + "messages.groupChatDetails.profileCard.optionsAriaLabel": "Show admin options", + "messages.groupChatDetails.removeAdminDialog.confirm": "Remove permissions", + "messages.groupChatDetails.removeAdminDialog.content": "This user will no longer be a group admin. You can always reassign them as admin later.", + "messages.groupChatDetails.removeAdminDialog.title": "Remove admin permissions", + "messages.groupChatEdit.addMemberCard.alreadyAdded": "Already added to the group", + "messages.groupChatEdit.addMembersDialog.title": "Add Members", + "messages.groupChatEdit.addMembersMobile.cancelAriaLabel": "cancel adding member", + "messages.groupChatEdit.header.cancelAriaLabel": "cancel editing group", + "messages.groupChatEdit.header.submitAriaLabel": "Edit group", + "messages.groupChatEdit.header.title": "Edit Group", + "messages.groupForm.validation.participantsRequired": "Please select at least one member", + "messages.groupForm.validation.titleMaxLength": "Title can't be more than 20 characters", + "messages.groupForm.validation.titleRequired": "Please enter a title", + "messages.groupMembersList.profileCard.add": "Add", + "messages.groupMembersList.profilesList.label": "Available connections", + "messages.groupMembersList.profilesList.title": "Connections", + "messages.groupMembersList.selectedMembersLabel": "Selected group members", + "messages.leaveGroupDialog.leave.content": "You will stop receiving messages from this conversation and people will see that you left.", + "messages.leaveGroupDialog.leave.title": "Leave group chat?", + "messages.leaveGroupDialog.leaveAsSoleAdmin.content": "You can choose a new admin from the people listed under members. If you leave the group without choosing a new admin, the most senior group member will become admin.", + "messages.leaveGroupDialog.leaveAsSoleAdmin.title": "Leave without choosing an admin?", + "messages.leaveGroupDialog.leaveGroupButton": "Leave group", + "messages.leaveGroupDialog.remove.content": "Are you sure you want to remove this person from the conversation? They will no longer be able to send or receive new messages.", + "messages.leaveGroupDialog.remove.title": "Remove group member?", + "messages.leaveGroupDialog.removeNamed.title": "Remove {name}?", + "messages.leaveGroupDialog.removeSuccess": "Member was successfully removed", + "messages.messageItem.actions.copy": "Copy", + "messages.messageItem.actions.downloadAttachments": "Download Attachments", + "messages.messageItem.copiedToast": "Message copied to clipboard.", + "messages.messageItem.deleteDialog.contentDirect": "Are you sure you want to delete this message? The message will be deleted for both you and the other person.", + "messages.messageItem.deleteDialog.contentGroup": "Are you sure you want to delete this message? The message will be deleted for everyone in this chat.", + "messages.messageItem.deleteDialog.title": "Delete message?", + "messages.messageItem.deletedToast": "Your message was deleted", + "messages.messageItem.itemTitle": "message", + "messages.messageUpdate.cancelAriaLabel": "cancel message edit", + "messages.messageUpdate.saveAriaLabel": "save message edit", + "messages.messagesGroup.newMessages": "New Messages", + "messages.messagesGroup.today": "Today", + "messages.messagesList.loadingMore": "loading more messages", + "messages.profileSummary.about": "About", + "messages.profileSummary.addToGroup": "Add contact to a group", + "messages.profileSummary.addToGroupAriaLabel": "add contact to a group", + "messages.profileSummary.goToProfile": "Go to profile", + "messages.profileSummary.goToProfileAriaLabel": "go to profile", + "messages.sendMessage.errorToast": "Your last message could not be sent. Please try again.", + "messages.shared.addMember": "Add Member", + "messages.shared.archiveChat": "Archive Chat", + "messages.shared.contactDetails": "Contact Details", + "messages.shared.deletedUser": "Deleted User", + "messages.shared.leaveGroup": "Leave Group", + "messages.shared.loadingMoreProfiles": "loading more profiles", + "messages.shared.memberCount": "{count, plural, one {# member} other {# members}}", + "messages.shared.members": "Members", + "messages.shared.remove": "Remove", + "messages.shared.returnToChatRooms": "return to existing chat rooms", + "messages.shared.seeProfile": "See Profile", + "messages.shared.somethingWentWrong": "Something went wrong", + "messages.shared.unarchiveChat": "Unarchive Chat", + "messages.shared.yesterday": "Yesterday", + "navigations.accountMenu.login": "Login", + "navigations.accountMenu.register": "Register", + "navigations.accountPopover.avatarAlt": "User avatar", + "navigations.accountPopover.logout": "Logout", + "navigations.verticalFooter.avatarAlt": "Profile avatar", + "notifications.commentCreated.message": "commented in {target, select, comment {your comment} contentpost {your contentpost} file {your file} page {your page} other {your {target}}}", + "notifications.commentReply.message": "replied to your comment", + "notifications.emptyState.description": "Your future notifications will be shown here.", + "notifications.emptyState.title": "You don’t have notifications.", + "notifications.item.avatarAlt": "Notification's user avatar", + "notifications.list.closeAriaLabel": "close notifications", + "notifications.list.markAllAsRead": "Mark all as read", + "notifications.list.older": "Older", + "notifications.list.title": "Notifications", + "notifications.popover.ariaLabel": "see notifications", + "notifications.popover.label": "Notifications", + "notifications.reactionCreated.message": "liked {target, select, comment {your comment} contentpost {your contentpost} file {your file} page {your page} other {your {target}}}", + "payments.addCard.addressMissing": "Address element is missing. Please try again.", + "payments.addCard.billingAddress": "Billing Address", + "payments.addCard.cardInformation": "Card Information", + "payments.addCard.confirmError": "Error confirming card: {message}", + "payments.addCard.genericError": "Error confirming card:", + "payments.addCard.incompleteAddress": "Error confirming card: Incomplete address", + "payments.addCard.success": "Card added successfully", + "payments.addCard.unknownError": "Unknown error", + "payments.addPaymentMethod": "Add payment method", + "payments.card.expired": "Expired", + "payments.card.expiredOn": "Expired: {month}/{year}", + "payments.card.expires": "Expires: {month}/{year}", + "payments.card.fallbackName": "Card", + "payments.checkout.address": "Address", + "payments.checkout.addressHelper": "Used to calculate taxes.", + "payments.checkout.consent": "By completing your purchase, you consent to BaseApp storing your payment method for future charges. You can change your payment method at any time in your account settings.", + "payments.checkout.createFailed": "Failed to create subscription: {message}", + "payments.checkout.paymentConfirmationFailed": "Payment confirmation failed: {message}", + "payments.checkout.placeOrder": "Place Order", + "payments.checkout.retry": "Retry", + "payments.checkout.taxesPerMonth": "+ taxes /month", + "payments.checkout.unexpectedError": "An unexpected error occurred. Please try again.", + "payments.checkout.updateFailed": "Failed to update subscription", + "payments.confirmation.description": "Thank you for your subscription! Access the plan details by clicking on the button below", + "payments.confirmation.descriptionWithEmail": "Thank you for your subscription! Access the plan details by clicking on the button below or in the email we just sent to.", + "payments.confirmation.orderNumber": "Order Number:", + "payments.confirmation.planDetails": "Plan Details", + "payments.confirmation.title": "Successfully Subscribed", + "payments.errors.tryAgain": "Please try again.", + "payments.freePlan.description": "Enjoy essential access to the platform at no cost. Perfect for exploring basic features and getting started.", + "payments.freePlan.features.coreFeatures": "Access to core features", + "payments.freePlan.features.limitedStorage": "Limited storage space", + "payments.freePlan.features.noCommitment": "No commitment required", + "payments.freePlan.features.standardSupport": "Standard support", + "payments.freePlan.title": "Free", + "payments.invoices.empty": "No receipts found", + "payments.invoices.header.amount": "Amount", + "payments.invoices.header.date": "Date", + "payments.invoices.header.description": "Description", + "payments.invoices.header.details": "Details", + "payments.invoices.header.info": "Info", + "payments.invoices.header.status": "Status", + "payments.invoices.receipt": "Receipt", + "payments.invoices.status.draft": "draft", + "payments.invoices.status.failed": "failed", + "payments.invoices.status.open": "open", + "payments.invoices.status.paid": "paid", + "payments.invoices.status.pending": "pending", + "payments.invoices.status.uncollectible": "uncollectible", + "payments.invoices.status.void": "void", + "payments.invoices.title": "Subscription Receipts", + "payments.payment": "Payment", + "payments.paymentDropdown.selectPaymentMethod": "Select the payment method", + "payments.paymentMethods.cardFallback": "CARD", + "payments.paymentMethods.default": "Default", + "payments.paymentMethods.description": "Add and manage your payment methods powered by Stripe.", + "payments.paymentMethods.remove": "Remove", + "payments.paymentMethods.removeDialog.content": "Are you sure you want to remove this payment method? If this is your only payment method, your payments will be interrupted until a new method is added.", + "payments.paymentMethods.removeDialog.title": "Remove Payment Method", + "payments.paymentMethods.removeFailed": "Failed to delete payment method: {message}", + "payments.paymentMethods.removeSuccess": "Payment method removed successfully", + "payments.paymentMethods.removing": "Removing...", + "payments.paymentMethods.setAsDefault": "Set as default", + "payments.paymentMethods.setDefaultFailed": "Failed to set default payment method: {message}", + "payments.paymentMethods.setDefaultSuccess": "Default payment method set successfully", + "payments.paymentMethods.title": "Payment Methods", + "payments.plans.manageSubscription": "Manage Subscription", + "payments.plans.monthly": "Monthly", + "payments.plans.perMonth": "USD/monthly", + "payments.plans.perYear": "USD/yearly", + "payments.plans.subscribe": "Subscribe", + "payments.plans.yearly": "Yearly", + "payments.status.active": "Active", + "payments.stripe.cancelSubscriptionFailed": "Failed to cancel subscription", + "payments.stripe.cancelSubscriptionSuccess": "Subscription cancelled successfully.", + "payments.stripe.confirmCardPaymentFailed": "Failed to confirm card payment: {message}", + "payments.stripe.confirmCardPaymentFailedFallback": "Failed to confirm card payment.", + "payments.subscription.cancel": "Cancel Subscription", + "payments.subscription.cancelDialog.back": "Back", + "payments.subscription.cancelDialog.content": "Are you sure you want to cancel your subscription? You will retain access to your current plan until the end of the billing period. After that, your account will be downgraded to the free plan.", + "payments.subscription.changePlan": "Change Plan", + "payments.subscription.nextBill": "Your next bill is for {amount} on {date}", + "payments.subscription.nextBillFree": "Your next bill is free on {date}", + "payments.subscription.planFallback": "Subscription Plan", + "payments.subscription.status.canceled": "Canceled", + "payments.subscription.status.pastDue": "Past Due", + "payments.subscription.status.pending": "Pending", + "payments.subscription.status.unpaid": "Unpaid", + "payments.subscription.title": "Subscription", + "payments.subscription.updatePaymentMethodFailed": "Failed to update payment method", + "payments.subscription.updated": "Subscription updated successfully.", + "profiles.blockDialog.block.action": "Block", + "profiles.blockDialog.block.content": "This person won’t be able to send you messages, find your profile or content. They won’t see your comments and reactions, and won't be able to mention or follow your profile. We won't notify them that they have been blocked.", + "profiles.blockDialog.block.title": "Block {name}?", + "profiles.blockDialog.blockProfile": "Block profile", + "profiles.blockDialog.blockedToast": "{name} is blocked", + "profiles.blockDialog.unblock.action": "Unblock", + "profiles.blockDialog.unblock.content": "This person will be able to send you messages, find your profile or content. They will see your comments and reactions, and will be able to mention or follow your profile. We won't notify them that they have been unblocked.", + "profiles.blockDialog.unblock.title": "Unblock {name}?", + "profiles.blockDialog.unblockProfile": "Unblock profile", + "profiles.blockDialog.unblockedToast": "{name} is unblocked", + "profiles.createProfile.description": "Create an organization and invite multiple members to manage and collaborate.", + "profiles.createProfile.errors.createFailed": "Unable to create organization. Please try again or contact support if the problem persists.", + "profiles.createProfile.errors.nameTaken": "An organization with this name already exists. Please choose a different name.", + "profiles.createProfile.errors.urlPathInvalid": "Invalid URL path format. Please use only lowercase letters, numbers, and hyphens.", + "profiles.createProfile.errors.urlPathTaken": "This URL path is already taken. Please choose a different one.", + "profiles.createProfile.name": "Name", + "profiles.createProfile.submit": "Create Organization", + "profiles.createProfile.terms": "Upon confirming, you agree to our Terms and Conditions.", + "profiles.createProfile.title": "New organization", + "profiles.createProfile.urlPath": "URL Path", + "profiles.createProfile.validation.nameMax": "Must have at most 255 characters", + "profiles.createProfile.validation.nameMin": "Must have at least three character", + "profiles.createProfile.validation.urlPathFormat": "Must contain only lowercase letters, numbers, and hyphens", + "profiles.createProfile.validation.urlPathMax": "Must have at most 500 characters", + "profiles.currentProfile.avatarAlt": "Current profile avatar", + "profiles.followToggle.follow": "Follow", + "profiles.inviteMemberDialog.addMembersFailed": "Failed to add members", + "profiles.inviteMemberDialog.description": "Add profiles to your organization or send an invitation email.", + "profiles.inviteMemberDialog.membersAdded": "{count, plural, one {Member added} other {# members added}}", + "profiles.inviteMemberDialog.removeMember": "Remove member", + "profiles.inviteMemberDialog.searchPlaceholder": "Invite members by name or email", + "profiles.inviteMemberDialog.sendInvitationsFailed": "Failed to send invitations", + "profiles.inviteMemberDialog.submit": "Invite", + "profiles.inviteMemberDialog.title": "Add member", + "profiles.members.actions.remove": "Remove", + "profiles.members.actions.resendInvitation": "Resend Invitation", + "profiles.members.addMember": "Add Member", + "profiles.members.avatarAlt": "Profile avatar", + "profiles.members.cancelInvitation.failed": "Invitation could not be removed", + "profiles.members.cancelInvitation.success": "Invitation removed", + "profiles.members.changeRoleDialog.content": "Are you sure you want to promote this member to an admin? They will have full administrative rights, including the ability to manage members and settings.", + "profiles.members.changeRoleDialog.title": "Change user permissions?", + "profiles.members.count": "{count, plural, one {# member} other {# members}}", + "profiles.members.dialogs.back": "Back", + "profiles.members.removeInvitationDialog.content": "Are you sure you want to remove this invitation? It will be deleted from the list.", + "profiles.members.removeInvitationDialog.title": "Remove invitation", + "profiles.members.removeMember.success": "Member removed successfully", + "profiles.members.removeMemberDialog.content": "Are you sure you want to remove this member? This action will revoke their access to the organization profile.", + "profiles.members.removeMemberDialog.title": "Remove member", + "profiles.members.resendInvitation.emailNotSent": "Invitation updated, but the email could not be sent", + "profiles.members.resendInvitation.success": "Invitation resent successfully", + "profiles.members.roles.admin": "Admin", + "profiles.members.roles.manager": "Manager", + "profiles.members.roles.owner": "Owner", + "profiles.members.statuses.active": "Active", + "profiles.members.statuses.declined": "Declined", + "profiles.members.statuses.expired": "Expired", + "profiles.members.statuses.inactive": "Inactive", + "profiles.members.statuses.pending": "Pending", + "profiles.members.title": "Members", + "profiles.permissionDenied.description": "You don't have permission to access this page. Please contact an administrator if you believe this is an error.", + "profiles.permissionDenied.goBack": "Go Back", + "profiles.permissionDenied.title": "Permission Required", + "profiles.profile.bannerAlt": "Home Banner", + "profiles.profile.editProfile": "Edit Profile", + "profiles.profile.followers": "Followers", + "profiles.profile.following": "Following", + "profiles.profile.shareCopied": "Profile URL copied to clipboard!", + "profiles.profile.shareProfile": "Share profile", + "profiles.profilesList.ariaLabel": "List of available profiles", + "profiles.profilesList.empty.description": "Your future profiles will be shown here.", + "profiles.profilesList.empty.title": "There are no profiles created.", + "profiles.profilesList.switchTo": "Switch to {name}", + "profiles.profilesList.switchToThisProfile": "Switch to this profile", + "profiles.profilesList.switchedTo": "Switched to {name}", + "profiles.report.alreadyReported": "You have already reported this profile.", + "profiles.report.anonymousNotice": "Your report is anonymous. If someone is in immediate danger, call the local emergency services - don't wait.", + "profiles.report.confirmation.title": "Thanks for reporting {reportType}", + "profiles.report.menuItem": "Report profile", + "profiles.report.summary.aboutProblem": "About the problem", + "profiles.report.summary.subTypeQuestion": "What type of {reportType}?", + "profiles.report.summary.submit": "Submit Report", + "profiles.report.summary.title": "You're about to submit a report", + "profiles.report.text.description": "Use the text field below to explain what the problem you are reporting is.", + "profiles.report.text.placeholder": "I find the post to be offensive...", + "profiles.report.text.title": "How would you describe the problem?", + "profiles.report.title": "Report", + "profiles.report.whyReporting": "Why are you reporting this?", + "profiles.settings.avatarAlt": "Avatar image", + "profiles.settings.bio": "Bio", + "profiles.settings.changeBanner": "Change Banner", + "profiles.settings.changeImage": "Change Image", + "profiles.settings.name": "Name", + "profiles.settings.phoneNumber": "Phone number", + "profiles.settings.remove": "Remove", + "profiles.settings.removeAvatarAriaLabel": "Remove avatar button", + "profiles.settings.removeBannerAriaLabel": "Remove banner button", + "profiles.settings.saveChanges": "Save Changes", + "profiles.settings.subtitle": "Manage your personal information you and other people see.", + "profiles.settings.title": "Profile", + "profiles.settings.updateError": "Something went wrong", + "profiles.settings.updateSuccess": "Profile updated", + "profiles.settings.uploadBanner": "Upload Banner", + "profiles.settings.uploadImage": "Upload Image", + "profiles.settings.username": "Username", + "profiles.settings.validation.nameEmpty": "Please enter a name.", + "profiles.settings.validation.urlPathEmpty": "Username must be at least 8 characters long.", + "profiles.settings.validation.urlPathInvalid": "Username can only contain letters and numbers", + "profiles.switchProfileMenu.label": "Switch Profile", + "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 1ec47af6c..106e54210 100644 --- a/packages/components/locales/es.json +++ b/packages/components/locales/es.json @@ -1,13 +1,418 @@ { + "activityLog.dateFilter.apply": "Filtrar", + "activityLog.dateFilter.clear": "Limpiar filtro", + "activityLog.dateFilter.endDate": "Fecha de fin", + "activityLog.dateFilter.error.invalidDate": "La fecha no es válida", + "activityLog.dateFilter.error.minDate": "La fecha de fin no puede ser anterior a la fecha de inicio.", + "activityLog.dateFilter.from": "Desde {date}", + "activityLog.dateFilter.notFound": "No se encontraron resultados para el rango de fechas seleccionado.", + "activityLog.dateFilter.period": "Período", + "activityLog.dateFilter.startDate": "Fecha de inicio", + "activityLog.dateFilter.until": "Hasta {date}", + "activityLog.eventFilter.label": "Filtro", + "activityLog.eventFilter.option.all": "Todos", + "activityLog.eventFilter.option.comments": "Comentarios", + "activityLog.eventFilter.option.posts": "Publicaciones", + "activityLog.eventFilter.option.reactions": "Reacciones", + "activityLog.loading.more": "cargando más registros de actividad", + "activityLog.logGroups.deletedUser": "Usuario eliminado", + "activityLog.logItem.updated": "Actualizó {name}", + "activityLog.logItem.updatedBio": "Actualizó su biografía", + "activityLog.logItem.updatedProfileBanner": "Actualizó su banner de perfil", + "activityLog.logItem.updatedProfilePicture": "Actualizó su foto de perfil", + "activityLog.logItem.verb.addComment": "Creó un comentario", + "activityLog.logItem.verb.addReaction": "Agregó una reacción", + "activityLog.logItem.verb.changeComment": "Editó un comentario", + "activityLog.logItem.verb.deleteComment": "Eliminó un comentario", + "activityLog.logItem.verb.pinComment": "Fijó un comentario", + "activityLog.logItem.verb.replyComment": "Respondió a un comentario", + "activityLog.search.placeholder": "Buscar por usuario", + "activityLog.title": "Registro de actividad", "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.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.reply.count.ariaLabel": "cantidad de respuestas {commentId}", + "comments.upsertActions.attachFiles.ariaLabel": "Adjuntar archivos", + "comments.upsertActions.mention.ariaLabel": "mención", + "contentFeed.loading.more": "cargando más publicaciones", + "contentFeed.newPost": "Nueva publicación", + "contentFeed.postCreate.error": "Algo salió mal", + "contentFeed.postCreate.success": "Publicación creada correctamente", + "contentFeed.postFooter.reply.ariaLabel": "Responder", + "contentFeed.postFooter.share.ariaLabel": "Compartir", + "contentFeed.postForm.content.placeholder": "¿Qué estás pensando?", + "contentFeed.postForm.disableReactions": "Desactivar reacciones en esta publicación", + "contentFeed.postForm.publish": "Publicar", + "contentFeed.postForm.title": "Nueva publicación", + "contentFeed.postHeader.options.ariaLabel": "Opciones de la publicación", + "contentFeed.postImageDropzone.title": "Haz clic para explorar o arrastra y suelta imágenes y videos.", + "contentFeed.postImageSlide.alt": "Imagen de la publicación", + "contentFeed.reaction.ariaLabel": "reaccionar a la publicación {postId}", + "contentFeed.reaction.count.ariaLabel": "cantidad de reacciones {postId}", + "contentFeed.title": "Feed de contenido", + "files.actions.download": "Descargar", + "files.actions.pause": "Pausar", + "files.actions.remove": "Quitar", + "files.actions.resume": "Reanudar", + "files.actions.retry": "Reintentar", + "files.attachedFileItem.status": "Estado: {status}", + "files.attachedFileItem.uploadedBy": "Subido por {name}", + "files.dropzone.subtitle": "Máximo {maxFiles} archivos, {maxFileSize} MB cada uno", + "files.dropzone.title": "Subir archivos", + "files.preview.alt": "Vista previa del archivo", + "files.progress.aborted": "Cancelado", + "files.progress.completed": "Completado", + "files.progress.failed": "Falló", + "files.progress.paused": "En pausa", + "files.progress.pending": "Pendiente...", + "files.progress.uploading": "Subiendo {progress}%", + "files.uploadList.attached": "Archivos adjuntos ({count})", + "files.uploadList.uploading": "Subiendo ({count})", + "files.uploadTrigger.label": "Adjuntar archivos", + "messages.addContactToGroup.groupItem.alreadyAdded": " • Ya agregado", + "messages.addContactToGroup.groupItem.selectAriaLabel": "seleccionar grupo {title}", + "messages.addContactToGroup.groupsList.empty.description": "Solo puedes agregar contactos a los grupos que administras.", + "messages.addContactToGroup.groupsList.empty.title": "Aún no hay grupos", + "messages.addContactToGroup.groupsList.loading": "cargando grupos", + "messages.addContactToGroup.groupsList.loadingMore": "cargando más grupos", + "messages.addContactToGroup.submit": "Agregar al grupo", + "messages.addContactToGroup.title": "Agregar contacto a un grupo", + "messages.chatCreate.header.title": "Nuevo chat", + "messages.chatCreate.item.message": "Mensaje", + "messages.chatCreate.newGroupButton": "Nuevo grupo", + "messages.chatRoom.header.backAriaLabel": "volver a la lista de chats", + "messages.chatRoom.header.optionsAriaLabel": "Mostrar opciones del chat", + "messages.chatRoom.notFound": "No se encontró el chat", + "messages.chatRoom.options.groupDetails": "Detalles del grupo", + "messages.chatRoomItem.date.daysAgo": "{count, plural, one {hace # día} other {hace # días}}", + "messages.chatRoomItem.date.monthsAgo": "{count, plural, one {hace # mes} other {hace # meses}}", + "messages.chatRoomItem.date.weeksAgo": "{count, plural, one {hace # semana} other {hace # semanas}}", + "messages.chatRoomItem.deleteDialog.title": "¿Eliminar chat?", + "messages.chatRoomItem.itemTitle": "Chat", + "messages.chatRoomItem.markAsUnread": "Marcar como no leído", + "messages.chatRoomsList.empty": "No hay mensajes para mostrar.", + "messages.chatRoomsList.loadingMore": "cargando más chats", + "messages.chatRoomsList.new": "Nuevo", + "messages.chatRoomsList.tabs.active": "Activos", + "messages.chatRoomsList.tabs.archived": "Archivados", + "messages.chatRoomsList.tabs.groups": "Grupos", + "messages.chatRoomsList.tabs.unread": "No leídos", + "messages.chatRoomsList.title": "Mensajes", + "messages.editGroupTitleAndImage.changeAvatar": "Cambiar avatar", + "messages.editGroupTitleAndImage.groupName": "Nombre del grupo", + "messages.editGroupTitleAndImage.uploadAvatar": "Subir avatar", + "messages.emptyProfilesList.description": "Cuando te conectes con otros perfiles, aparecerán aquí.", + "messages.emptyProfilesList.title": "Busca perfiles", + "messages.groupChatCreate.header.cancelAriaLabel": "cancelar la creación del grupo", + "messages.groupChatCreate.header.createAriaLabel": "Crear grupo", + "messages.groupChatCreate.header.title": "Nuevo grupo", + "messages.groupChatDetails.adminOptions.promoteToAdmin": "Hacer administrador", + "messages.groupChatDetails.adminOptions.removeAdmin": "Quitar permisos de administrador", + "messages.groupChatDetails.adminOptions.removeFromGroup": "Quitar del grupo", + "messages.groupChatDetails.header.editAriaLabel": "editar chat grupal", + "messages.groupChatDetails.header.title": "Vista del grupo", + "messages.groupChatDetails.membersAriaLabel": "miembros del grupo", + "messages.groupChatDetails.profileCard.admin": "Administrador", + "messages.groupChatDetails.profileCard.optionsAriaLabel": "Mostrar opciones de administrador", + "messages.groupChatDetails.removeAdminDialog.confirm": "Quitar permisos", + "messages.groupChatDetails.removeAdminDialog.content": "Este usuario dejará de ser administrador del grupo. Siempre puedes volver a asignarlo como administrador más tarde.", + "messages.groupChatDetails.removeAdminDialog.title": "Quitar permisos de administrador", + "messages.groupChatEdit.addMemberCard.alreadyAdded": "Ya está en el grupo", + "messages.groupChatEdit.addMembersDialog.title": "Agregar miembros", + "messages.groupChatEdit.addMembersMobile.cancelAriaLabel": "cancelar agregar miembro", + "messages.groupChatEdit.header.cancelAriaLabel": "cancelar la edición del grupo", + "messages.groupChatEdit.header.submitAriaLabel": "Editar grupo", + "messages.groupChatEdit.header.title": "Editar grupo", + "messages.groupForm.validation.participantsRequired": "Selecciona al menos un miembro", + "messages.groupForm.validation.titleMaxLength": "El título no puede tener más de 20 caracteres", + "messages.groupForm.validation.titleRequired": "Ingresa un título", + "messages.groupMembersList.profileCard.add": "Agregar", + "messages.groupMembersList.profilesList.label": "Conexiones disponibles", + "messages.groupMembersList.profilesList.title": "Conexiones", + "messages.groupMembersList.selectedMembersLabel": "Miembros del grupo seleccionados", + "messages.leaveGroupDialog.leave.content": "Dejarás de recibir mensajes de esta conversación y las personas verán que saliste.", + "messages.leaveGroupDialog.leave.title": "¿Salir del chat grupal?", + "messages.leaveGroupDialog.leaveAsSoleAdmin.content": "Puedes elegir un nuevo administrador entre las personas que aparecen en miembros. Si sales del grupo sin elegir un nuevo administrador, el miembro más antiguo del grupo se convertirá en administrador.", + "messages.leaveGroupDialog.leaveAsSoleAdmin.title": "¿Salir sin elegir un administrador?", + "messages.leaveGroupDialog.leaveGroupButton": "Salir del grupo", + "messages.leaveGroupDialog.remove.content": "¿Seguro que quieres quitar a esta persona de la conversación? Ya no podrá enviar ni recibir mensajes nuevos.", + "messages.leaveGroupDialog.remove.title": "¿Quitar miembro del grupo?", + "messages.leaveGroupDialog.removeNamed.title": "¿Quitar a {name}?", + "messages.leaveGroupDialog.removeSuccess": "El miembro se quitó correctamente", + "messages.messageItem.actions.copy": "Copiar", + "messages.messageItem.actions.downloadAttachments": "Descargar adjuntos", + "messages.messageItem.copiedToast": "Mensaje copiado al portapapeles.", + "messages.messageItem.deleteDialog.contentDirect": "¿Seguro que quieres eliminar este mensaje? Se eliminará para ti y para la otra persona.", + "messages.messageItem.deleteDialog.contentGroup": "¿Seguro que quieres eliminar este mensaje? Se eliminará para todos en este chat.", + "messages.messageItem.deleteDialog.title": "¿Eliminar mensaje?", + "messages.messageItem.deletedToast": "Tu mensaje fue eliminado", + "messages.messageItem.itemTitle": "mensaje", + "messages.messageUpdate.cancelAriaLabel": "cancelar la edición del mensaje", + "messages.messageUpdate.saveAriaLabel": "guardar la edición del mensaje", + "messages.messagesGroup.newMessages": "Mensajes nuevos", + "messages.messagesGroup.today": "Hoy", + "messages.messagesList.loadingMore": "cargando más mensajes", + "messages.profileSummary.about": "Acerca de", + "messages.profileSummary.addToGroup": "Agregar contacto a un grupo", + "messages.profileSummary.addToGroupAriaLabel": "agregar contacto a un grupo", + "messages.profileSummary.goToProfile": "Ir al perfil", + "messages.profileSummary.goToProfileAriaLabel": "ir al perfil", + "messages.sendMessage.errorToast": "No se pudo enviar tu último mensaje. Inténtalo de nuevo.", + "messages.shared.addMember": "Agregar miembro", + "messages.shared.archiveChat": "Archivar chat", + "messages.shared.contactDetails": "Detalles del contacto", + "messages.shared.deletedUser": "Usuario eliminado", + "messages.shared.leaveGroup": "Salir del grupo", + "messages.shared.loadingMoreProfiles": "cargando más perfiles", + "messages.shared.memberCount": "{count, plural, one {# miembro} other {# miembros}}", + "messages.shared.members": "Miembros", + "messages.shared.remove": "Quitar", + "messages.shared.returnToChatRooms": "volver a los chats existentes", + "messages.shared.seeProfile": "Ver perfil", + "messages.shared.somethingWentWrong": "Algo salió mal", + "messages.shared.unarchiveChat": "Desarchivar chat", + "messages.shared.yesterday": "Ayer", + "navigations.accountMenu.login": "Iniciar sesión", + "navigations.accountMenu.register": "Registrarse", + "navigations.accountPopover.avatarAlt": "Avatar del usuario", + "navigations.accountPopover.logout": "Cerrar sesión", + "navigations.verticalFooter.avatarAlt": "Avatar del perfil", + "notifications.commentCreated.message": "comentó en {target, select, comment {tu comentario} contentpost {tu publicación} file {tu archivo} page {tu página} other {tu {target}}}", + "notifications.commentReply.message": "respondió a tu comentario", + "notifications.emptyState.description": "Tus próximas notificaciones aparecerán aquí.", + "notifications.emptyState.title": "No tienes notificaciones.", + "notifications.item.avatarAlt": "Avatar del usuario de la notificación", + "notifications.list.closeAriaLabel": "cerrar notificaciones", + "notifications.list.markAllAsRead": "Marcar todas como leídas", + "notifications.list.older": "Anteriores", + "notifications.list.title": "Notificaciones", + "notifications.popover.ariaLabel": "ver notificaciones", + "notifications.popover.label": "Notificaciones", + "notifications.reactionCreated.message": "reaccionó a {target, select, comment {tu comentario} contentpost {tu publicación} file {tu archivo} page {tu página} other {tu {target}}}", + "payments.addCard.addressMissing": "Falta el elemento de dirección. Inténtalo de nuevo.", + "payments.addCard.billingAddress": "Dirección de facturación", + "payments.addCard.cardInformation": "Información de la tarjeta", + "payments.addCard.confirmError": "Error al confirmar la tarjeta: {message}", + "payments.addCard.genericError": "Error al confirmar la tarjeta:", + "payments.addCard.incompleteAddress": "Error al confirmar la tarjeta: dirección incompleta", + "payments.addCard.success": "Tarjeta agregada correctamente", + "payments.addCard.unknownError": "Error desconocido", + "payments.addPaymentMethod": "Agregar método de pago", + "payments.card.expired": "Vencida", + "payments.card.expiredOn": "Venció: {month}/{year}", + "payments.card.expires": "Vence: {month}/{year}", + "payments.card.fallbackName": "Tarjeta", + "payments.checkout.address": "Dirección", + "payments.checkout.addressHelper": "Se usa para calcular los impuestos.", + "payments.checkout.consent": "Al completar tu compra, aceptas que BaseApp guarde tu método de pago para cargos futuros. Puedes cambiar tu método de pago en cualquier momento en la configuración de tu cuenta.", + "payments.checkout.createFailed": "No se pudo crear la suscripción: {message}", + "payments.checkout.paymentConfirmationFailed": "Falló la confirmación del pago: {message}", + "payments.checkout.placeOrder": "Realizar pedido", + "payments.checkout.retry": "Reintentar", + "payments.checkout.taxesPerMonth": "+ impuestos /mes", + "payments.checkout.unexpectedError": "Ocurrió un error inesperado. Inténtalo de nuevo.", + "payments.checkout.updateFailed": "No se pudo actualizar la suscripción", + "payments.confirmation.description": "¡Gracias por tu suscripción! Accede a los detalles del plan haciendo clic en el botón de abajo", + "payments.confirmation.descriptionWithEmail": "¡Gracias por tu suscripción! Accede a los detalles del plan haciendo clic en el botón de abajo o en el correo electrónico que acabamos de enviar a.", + "payments.confirmation.orderNumber": "Número de pedido:", + "payments.confirmation.planDetails": "Detalles del plan", + "payments.confirmation.title": "Suscripción exitosa", + "payments.errors.tryAgain": "Inténtalo de nuevo.", + "payments.freePlan.description": "Disfruta del acceso esencial a la plataforma sin costo. Ideal para explorar las funciones básicas y comenzar.", + "payments.freePlan.features.coreFeatures": "Acceso a las funciones principales", + "payments.freePlan.features.limitedStorage": "Espacio de almacenamiento limitado", + "payments.freePlan.features.noCommitment": "Sin compromiso", + "payments.freePlan.features.standardSupport": "Soporte estándar", + "payments.freePlan.title": "Gratis", + "payments.invoices.empty": "No se encontraron recibos", + "payments.invoices.header.amount": "Monto", + "payments.invoices.header.date": "Fecha", + "payments.invoices.header.description": "Descripción", + "payments.invoices.header.details": "Detalles", + "payments.invoices.header.info": "Información", + "payments.invoices.header.status": "Estado", + "payments.invoices.receipt": "Recibo", + "payments.invoices.status.draft": "borrador", + "payments.invoices.status.failed": "fallido", + "payments.invoices.status.open": "abierto", + "payments.invoices.status.paid": "pagado", + "payments.invoices.status.pending": "pendiente", + "payments.invoices.status.uncollectible": "incobrable", + "payments.invoices.status.void": "anulado", + "payments.invoices.title": "Recibos de suscripción", + "payments.payment": "Pago", + "payments.paymentDropdown.selectPaymentMethod": "Selecciona el método de pago", + "payments.paymentMethods.cardFallback": "TARJETA", + "payments.paymentMethods.default": "Predeterminado", + "payments.paymentMethods.description": "Agrega y administra tus métodos de pago con Stripe.", + "payments.paymentMethods.remove": "Quitar", + "payments.paymentMethods.removeDialog.content": "¿Seguro que quieres quitar este método de pago? Si es tu único método de pago, tus pagos se interrumpirán hasta que agregues uno nuevo.", + "payments.paymentMethods.removeDialog.title": "Quitar método de pago", + "payments.paymentMethods.removeFailed": "No se pudo eliminar el método de pago: {message}", + "payments.paymentMethods.removeSuccess": "Método de pago quitado correctamente", + "payments.paymentMethods.removing": "Quitando...", + "payments.paymentMethods.setAsDefault": "Establecer como predeterminado", + "payments.paymentMethods.setDefaultFailed": "No se pudo establecer el método de pago predeterminado: {message}", + "payments.paymentMethods.setDefaultSuccess": "Método de pago predeterminado establecido correctamente", + "payments.paymentMethods.title": "Métodos de pago", + "payments.plans.manageSubscription": "Administrar suscripción", + "payments.plans.monthly": "Mensual", + "payments.plans.perMonth": "USD/mes", + "payments.plans.perYear": "USD/año", + "payments.plans.subscribe": "Suscribirse", + "payments.plans.yearly": "Anual", + "payments.status.active": "Activa", + "payments.stripe.cancelSubscriptionFailed": "No se pudo cancelar la suscripción", + "payments.stripe.cancelSubscriptionSuccess": "Suscripción cancelada correctamente.", + "payments.stripe.confirmCardPaymentFailed": "No se pudo confirmar el pago con tarjeta: {message}", + "payments.stripe.confirmCardPaymentFailedFallback": "No se pudo confirmar el pago con tarjeta.", + "payments.subscription.cancel": "Cancelar suscripción", + "payments.subscription.cancelDialog.back": "Volver", + "payments.subscription.cancelDialog.content": "¿Seguro que quieres cancelar tu suscripción? Mantendrás el acceso a tu plan actual hasta el final del período de facturación. Después, tu cuenta pasará al plan gratuito.", + "payments.subscription.changePlan": "Cambiar plan", + "payments.subscription.nextBill": "Tu próxima factura es de {amount} el {date}", + "payments.subscription.nextBillFree": "Tu próxima factura es gratis el {date}", + "payments.subscription.planFallback": "Plan de suscripción", + "payments.subscription.status.canceled": "Cancelada", + "payments.subscription.status.pastDue": "Vencida", + "payments.subscription.status.pending": "Pendiente", + "payments.subscription.status.unpaid": "Impaga", + "payments.subscription.title": "Suscripción", + "payments.subscription.updatePaymentMethodFailed": "No se pudo actualizar el método de pago", + "payments.subscription.updated": "Suscripción actualizada correctamente.", + "profiles.blockDialog.block.action": "Bloquear", + "profiles.blockDialog.block.content": "Esta persona no podrá enviarte mensajes ni encontrar tu perfil o contenido. No verá tus comentarios ni reacciones, y no podrá mencionarte ni seguir tu perfil. No le notificaremos que la bloqueaste.", + "profiles.blockDialog.block.title": "¿Bloquear a {name}?", + "profiles.blockDialog.blockProfile": "Bloquear perfil", + "profiles.blockDialog.blockedToast": "{name} está bloqueado", + "profiles.blockDialog.unblock.action": "Desbloquear", + "profiles.blockDialog.unblock.content": "Esta persona podrá enviarte mensajes y encontrar tu perfil o contenido. Verá tus comentarios y reacciones, y podrá mencionarte o seguir tu perfil. No le notificaremos que la desbloqueaste.", + "profiles.blockDialog.unblock.title": "¿Desbloquear a {name}?", + "profiles.blockDialog.unblockProfile": "Desbloquear perfil", + "profiles.blockDialog.unblockedToast": "{name} está desbloqueado", + "profiles.createProfile.description": "Crea una organización e invita a varios miembros para administrar y colaborar.", + "profiles.createProfile.errors.createFailed": "No se pudo crear la organización. Inténtalo de nuevo o contacta a soporte si el problema persiste.", + "profiles.createProfile.errors.nameTaken": "Ya existe una organización con este nombre. Elige un nombre diferente.", + "profiles.createProfile.errors.urlPathInvalid": "Formato de ruta URL no válido. Usa solo letras minúsculas, números y guiones.", + "profiles.createProfile.errors.urlPathTaken": "Esta ruta URL ya está en uso. Elige otra.", + "profiles.createProfile.name": "Nombre", + "profiles.createProfile.submit": "Crear organización", + "profiles.createProfile.terms": "Al confirmar, aceptas nuestros Términos y Condiciones.", + "profiles.createProfile.title": "Nueva organización", + "profiles.createProfile.urlPath": "Ruta URL", + "profiles.createProfile.validation.nameMax": "Debe tener como máximo 255 caracteres", + "profiles.createProfile.validation.nameMin": "Debe tener al menos tres caracteres", + "profiles.createProfile.validation.urlPathFormat": "Debe contener solo letras minúsculas, números y guiones", + "profiles.createProfile.validation.urlPathMax": "Debe tener como máximo 500 caracteres", + "profiles.currentProfile.avatarAlt": "Avatar del perfil actual", + "profiles.followToggle.follow": "Seguir", + "profiles.inviteMemberDialog.addMembersFailed": "No se pudieron agregar los miembros", + "profiles.inviteMemberDialog.description": "Agrega perfiles a tu organización o envía una invitación por correo electrónico.", + "profiles.inviteMemberDialog.membersAdded": "{count, plural, one {Miembro agregado} other {# miembros agregados}}", + "profiles.inviteMemberDialog.removeMember": "Quitar miembro", + "profiles.inviteMemberDialog.searchPlaceholder": "Invita miembros por nombre o correo electrónico", + "profiles.inviteMemberDialog.sendInvitationsFailed": "No se pudieron enviar las invitaciones", + "profiles.inviteMemberDialog.submit": "Invitar", + "profiles.inviteMemberDialog.title": "Agregar miembro", + "profiles.members.actions.remove": "Quitar", + "profiles.members.actions.resendInvitation": "Reenviar invitación", + "profiles.members.addMember": "Agregar miembro", + "profiles.members.avatarAlt": "Avatar del perfil", + "profiles.members.cancelInvitation.failed": "No se pudo quitar la invitación", + "profiles.members.cancelInvitation.success": "Invitación quitada", + "profiles.members.changeRoleDialog.content": "¿Seguro que quieres promover a este miembro a administrador? Tendrá todos los permisos de administración, incluida la capacidad de gestionar miembros y la configuración.", + "profiles.members.changeRoleDialog.title": "¿Cambiar los permisos del usuario?", + "profiles.members.count": "{count, plural, one {# miembro} other {# miembros}}", + "profiles.members.dialogs.back": "Volver", + "profiles.members.removeInvitationDialog.content": "¿Seguro que quieres quitar esta invitación? Se eliminará de la lista.", + "profiles.members.removeInvitationDialog.title": "Quitar invitación", + "profiles.members.removeMember.success": "Miembro quitado correctamente", + "profiles.members.removeMemberDialog.content": "¿Seguro que quieres quitar a este miembro? Esta acción revocará su acceso al perfil de la organización.", + "profiles.members.removeMemberDialog.title": "Quitar miembro", + "profiles.members.resendInvitation.emailNotSent": "La invitación se actualizó, pero no se pudo enviar el correo electrónico", + "profiles.members.resendInvitation.success": "Invitación reenviada correctamente", + "profiles.members.roles.admin": "Administrador", + "profiles.members.roles.manager": "Gestor", + "profiles.members.roles.owner": "Propietario", + "profiles.members.statuses.active": "Activo", + "profiles.members.statuses.declined": "Rechazada", + "profiles.members.statuses.expired": "Vencida", + "profiles.members.statuses.inactive": "Inactivo", + "profiles.members.statuses.pending": "Pendiente", + "profiles.members.title": "Miembros", + "profiles.permissionDenied.description": "No tienes permiso para acceder a esta página. Contacta a un administrador si crees que se trata de un error.", + "profiles.permissionDenied.goBack": "Volver", + "profiles.permissionDenied.title": "Se requiere permiso", + "profiles.profile.bannerAlt": "Banner de inicio", + "profiles.profile.editProfile": "Editar perfil", + "profiles.profile.followers": "Seguidores", + "profiles.profile.following": "Siguiendo", + "profiles.profile.shareCopied": "¡URL del perfil copiada al portapapeles!", + "profiles.profile.shareProfile": "Compartir perfil", + "profiles.profilesList.ariaLabel": "Lista de perfiles disponibles", + "profiles.profilesList.empty.description": "Tus futuros perfiles aparecerán aquí.", + "profiles.profilesList.empty.title": "No hay perfiles creados.", + "profiles.profilesList.switchTo": "Cambiar a {name}", + "profiles.profilesList.switchToThisProfile": "Cambiar a este perfil", + "profiles.profilesList.switchedTo": "Cambiaste a {name}", + "profiles.report.alreadyReported": "Ya reportaste este perfil.", + "profiles.report.anonymousNotice": "Tu reporte es anónimo. Si alguien está en peligro inmediato, llama a los servicios de emergencia locales; no esperes.", + "profiles.report.confirmation.title": "Gracias por reportar {reportType}", + "profiles.report.menuItem": "Reportar perfil", + "profiles.report.summary.aboutProblem": "Sobre el problema", + "profiles.report.summary.subTypeQuestion": "¿Qué tipo de {reportType}?", + "profiles.report.summary.submit": "Enviar reporte", + "profiles.report.summary.title": "Estás a punto de enviar un reporte", + "profiles.report.text.description": "Usa el campo de texto de abajo para explicar cuál es el problema que estás reportando.", + "profiles.report.text.placeholder": "Considero que la publicación es ofensiva...", + "profiles.report.text.title": "¿Cómo describirías el problema?", + "profiles.report.title": "Reportar", + "profiles.report.whyReporting": "¿Por qué reportas esto?", + "profiles.settings.avatarAlt": "Imagen de avatar", + "profiles.settings.bio": "Biografía", + "profiles.settings.changeBanner": "Cambiar banner", + "profiles.settings.changeImage": "Cambiar imagen", + "profiles.settings.name": "Nombre", + "profiles.settings.phoneNumber": "Número de teléfono", + "profiles.settings.remove": "Quitar", + "profiles.settings.removeAvatarAriaLabel": "Botón para quitar el avatar", + "profiles.settings.removeBannerAriaLabel": "Botón para quitar el banner", + "profiles.settings.saveChanges": "Guardar cambios", + "profiles.settings.subtitle": "Administra la información personal que ves tú y otras personas.", + "profiles.settings.title": "Perfil", + "profiles.settings.updateError": "Algo salió mal", + "profiles.settings.updateSuccess": "Perfil actualizado", + "profiles.settings.uploadBanner": "Subir banner", + "profiles.settings.uploadImage": "Subir imagen", + "profiles.settings.username": "Nombre de usuario", + "profiles.settings.validation.nameEmpty": "Ingresa un nombre.", + "profiles.settings.validation.urlPathEmpty": "El nombre de usuario debe tener al menos 8 caracteres.", + "profiles.settings.validation.urlPathInvalid": "El nombre de usuario solo puede contener letras y números", + "profiles.switchProfileMenu.label": "Cambiar perfil", + "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 73de7836d..68dfc16ed 100644 --- a/packages/components/locales/pt.json +++ b/packages/components/locales/pt.json @@ -1,13 +1,418 @@ { + "activityLog.dateFilter.apply": "Filtrar", + "activityLog.dateFilter.clear": "Limpar filtro", + "activityLog.dateFilter.endDate": "Data final", + "activityLog.dateFilter.error.invalidDate": "A data não é válida", + "activityLog.dateFilter.error.minDate": "A data final não pode ser anterior à data inicial.", + "activityLog.dateFilter.from": "A partir de {date}", + "activityLog.dateFilter.notFound": "Nenhum resultado encontrado para o período selecionado.", + "activityLog.dateFilter.period": "Período", + "activityLog.dateFilter.startDate": "Data inicial", + "activityLog.dateFilter.until": "Até {date}", + "activityLog.eventFilter.label": "Filtro", + "activityLog.eventFilter.option.all": "Todos", + "activityLog.eventFilter.option.comments": "Comentários", + "activityLog.eventFilter.option.posts": "Publicações", + "activityLog.eventFilter.option.reactions": "Reações", + "activityLog.loading.more": "carregando mais registros de atividades", + "activityLog.logGroups.deletedUser": "Usuário excluído", + "activityLog.logItem.updated": "Atualizou {name}", + "activityLog.logItem.updatedBio": "Atualizou a biografia", + "activityLog.logItem.updatedProfileBanner": "Atualizou o banner do perfil", + "activityLog.logItem.updatedProfilePicture": "Atualizou a foto do perfil", + "activityLog.logItem.verb.addComment": "Criou um comentário", + "activityLog.logItem.verb.addReaction": "Adicionou uma reação", + "activityLog.logItem.verb.changeComment": "Editou um comentário", + "activityLog.logItem.verb.deleteComment": "Excluiu um comentário", + "activityLog.logItem.verb.pinComment": "Fixou um comentário", + "activityLog.logItem.verb.replyComment": "Respondeu a um comentário", + "activityLog.search.placeholder": "Pesquisar por usuário", + "activityLog.title": "Registro de atividades", "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.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.reply.count.ariaLabel": "contagem de respostas {commentId}", + "comments.upsertActions.attachFiles.ariaLabel": "Anexar arquivos", + "comments.upsertActions.mention.ariaLabel": "menção", + "contentFeed.loading.more": "carregando mais publicações", + "contentFeed.newPost": "Nova publicação", + "contentFeed.postCreate.error": "Algo deu errado", + "contentFeed.postCreate.success": "Publicação criada com sucesso", + "contentFeed.postFooter.reply.ariaLabel": "Responder", + "contentFeed.postFooter.share.ariaLabel": "Compartilhar", + "contentFeed.postForm.content.placeholder": "No que você está pensando?", + "contentFeed.postForm.disableReactions": "Desativar reações nesta publicação", + "contentFeed.postForm.publish": "Publicar", + "contentFeed.postForm.title": "Nova publicação", + "contentFeed.postHeader.options.ariaLabel": "Opções da publicação", + "contentFeed.postImageDropzone.title": "Clique para procurar ou arraste e solte imagens e vídeos.", + "contentFeed.postImageSlide.alt": "Imagem da publicação", + "contentFeed.reaction.ariaLabel": "reagir à publicação {postId}", + "contentFeed.reaction.count.ariaLabel": "número de reações {postId}", + "contentFeed.title": "Feed de conteúdo", + "files.actions.download": "Baixar", + "files.actions.pause": "Pausar", + "files.actions.remove": "Remover", + "files.actions.resume": "Retomar", + "files.actions.retry": "Tentar novamente", + "files.attachedFileItem.status": "Status: {status}", + "files.attachedFileItem.uploadedBy": "Enviado por {name}", + "files.dropzone.subtitle": "Máximo de {maxFiles} arquivos, {maxFileSize} MB cada", + "files.dropzone.title": "Enviar arquivos", + "files.preview.alt": "Pré-visualização do arquivo", + "files.progress.aborted": "Cancelado", + "files.progress.completed": "Concluído", + "files.progress.failed": "Falhou", + "files.progress.paused": "Pausado", + "files.progress.pending": "Pendente...", + "files.progress.uploading": "Enviando {progress}%", + "files.uploadList.attached": "Arquivos anexados ({count})", + "files.uploadList.uploading": "Enviando ({count})", + "files.uploadTrigger.label": "Anexar arquivos", + "messages.addContactToGroup.groupItem.alreadyAdded": " • Já adicionado", + "messages.addContactToGroup.groupItem.selectAriaLabel": "selecionar grupo {title}", + "messages.addContactToGroup.groupsList.empty.description": "Você só pode adicionar contatos a grupos que você administra.", + "messages.addContactToGroup.groupsList.empty.title": "Nenhum grupo ainda", + "messages.addContactToGroup.groupsList.loading": "carregando grupos", + "messages.addContactToGroup.groupsList.loadingMore": "carregando mais grupos", + "messages.addContactToGroup.submit": "Adicionar ao grupo", + "messages.addContactToGroup.title": "Adicionar contato a um grupo", + "messages.chatCreate.header.title": "Nova conversa", + "messages.chatCreate.item.message": "Mensagem", + "messages.chatCreate.newGroupButton": "Novo grupo", + "messages.chatRoom.header.backAriaLabel": "voltar para a lista de conversas", + "messages.chatRoom.header.optionsAriaLabel": "Mostrar opções da conversa", + "messages.chatRoom.notFound": "Conversa não encontrada", + "messages.chatRoom.options.groupDetails": "Detalhes do grupo", + "messages.chatRoomItem.date.daysAgo": "{count, plural, one {há # dia} other {há # dias}}", + "messages.chatRoomItem.date.monthsAgo": "{count, plural, one {há # mês} other {há # meses}}", + "messages.chatRoomItem.date.weeksAgo": "{count, plural, one {há # semana} other {há # semanas}}", + "messages.chatRoomItem.deleteDialog.title": "Excluir conversa?", + "messages.chatRoomItem.itemTitle": "Conversa", + "messages.chatRoomItem.markAsUnread": "Marcar como não lida", + "messages.chatRoomsList.empty": "Nenhuma mensagem para exibir.", + "messages.chatRoomsList.loadingMore": "carregando mais conversas", + "messages.chatRoomsList.new": "Nova", + "messages.chatRoomsList.tabs.active": "Ativas", + "messages.chatRoomsList.tabs.archived": "Arquivadas", + "messages.chatRoomsList.tabs.groups": "Grupos", + "messages.chatRoomsList.tabs.unread": "Não lidas", + "messages.chatRoomsList.title": "Mensagens", + "messages.editGroupTitleAndImage.changeAvatar": "Alterar avatar", + "messages.editGroupTitleAndImage.groupName": "Nome do grupo", + "messages.editGroupTitleAndImage.uploadAvatar": "Enviar avatar", + "messages.emptyProfilesList.description": "Quando você se conectar a outros perfis, eles aparecerão aqui.", + "messages.emptyProfilesList.title": "Pesquise perfis", + "messages.groupChatCreate.header.cancelAriaLabel": "cancelar criação do grupo", + "messages.groupChatCreate.header.createAriaLabel": "Criar grupo", + "messages.groupChatCreate.header.title": "Novo grupo", + "messages.groupChatDetails.adminOptions.promoteToAdmin": "Tornar administrador", + "messages.groupChatDetails.adminOptions.removeAdmin": "Remover permissões de administrador", + "messages.groupChatDetails.adminOptions.removeFromGroup": "Remover do grupo", + "messages.groupChatDetails.header.editAriaLabel": "editar conversa em grupo", + "messages.groupChatDetails.header.title": "Visão do grupo", + "messages.groupChatDetails.membersAriaLabel": "membros do grupo", + "messages.groupChatDetails.profileCard.admin": "Administrador", + "messages.groupChatDetails.profileCard.optionsAriaLabel": "Mostrar opções de administrador", + "messages.groupChatDetails.removeAdminDialog.confirm": "Remover permissões", + "messages.groupChatDetails.removeAdminDialog.content": "Este usuário deixará de ser administrador do grupo. Você pode torná-lo administrador novamente depois.", + "messages.groupChatDetails.removeAdminDialog.title": "Remover permissões de administrador", + "messages.groupChatEdit.addMemberCard.alreadyAdded": "Já adicionado ao grupo", + "messages.groupChatEdit.addMembersDialog.title": "Adicionar membros", + "messages.groupChatEdit.addMembersMobile.cancelAriaLabel": "cancelar adição de membro", + "messages.groupChatEdit.header.cancelAriaLabel": "cancelar edição do grupo", + "messages.groupChatEdit.header.submitAriaLabel": "Editar grupo", + "messages.groupChatEdit.header.title": "Editar grupo", + "messages.groupForm.validation.participantsRequired": "Selecione pelo menos um membro", + "messages.groupForm.validation.titleMaxLength": "O título não pode ter mais de 20 caracteres", + "messages.groupForm.validation.titleRequired": "Informe um título", + "messages.groupMembersList.profileCard.add": "Adicionar", + "messages.groupMembersList.profilesList.label": "Conexões disponíveis", + "messages.groupMembersList.profilesList.title": "Conexões", + "messages.groupMembersList.selectedMembersLabel": "Membros do grupo selecionados", + "messages.leaveGroupDialog.leave.content": "Você deixará de receber mensagens desta conversa e as pessoas verão que você saiu.", + "messages.leaveGroupDialog.leave.title": "Sair da conversa em grupo?", + "messages.leaveGroupDialog.leaveAsSoleAdmin.content": "Você pode escolher um novo administrador entre as pessoas listadas em membros. Se você sair do grupo sem escolher um novo administrador, o membro mais antigo do grupo se tornará administrador.", + "messages.leaveGroupDialog.leaveAsSoleAdmin.title": "Sair sem escolher um administrador?", + "messages.leaveGroupDialog.leaveGroupButton": "Sair do grupo", + "messages.leaveGroupDialog.remove.content": "Tem certeza de que deseja remover esta pessoa da conversa? Ela não poderá mais enviar nem receber novas mensagens.", + "messages.leaveGroupDialog.remove.title": "Remover membro do grupo?", + "messages.leaveGroupDialog.removeNamed.title": "Remover {name}?", + "messages.leaveGroupDialog.removeSuccess": "Membro removido com sucesso", + "messages.messageItem.actions.copy": "Copiar", + "messages.messageItem.actions.downloadAttachments": "Baixar anexos", + "messages.messageItem.copiedToast": "Mensagem copiada para a área de transferência.", + "messages.messageItem.deleteDialog.contentDirect": "Tem certeza de que deseja excluir esta mensagem? Ela será excluída para você e para a outra pessoa.", + "messages.messageItem.deleteDialog.contentGroup": "Tem certeza de que deseja excluir esta mensagem? Ela será excluída para todos nesta conversa.", + "messages.messageItem.deleteDialog.title": "Excluir mensagem?", + "messages.messageItem.deletedToast": "Sua mensagem foi excluída", + "messages.messageItem.itemTitle": "mensagem", + "messages.messageUpdate.cancelAriaLabel": "cancelar edição da mensagem", + "messages.messageUpdate.saveAriaLabel": "salvar edição da mensagem", + "messages.messagesGroup.newMessages": "Novas mensagens", + "messages.messagesGroup.today": "Hoje", + "messages.messagesList.loadingMore": "carregando mais mensagens", + "messages.profileSummary.about": "Sobre", + "messages.profileSummary.addToGroup": "Adicionar contato a um grupo", + "messages.profileSummary.addToGroupAriaLabel": "adicionar contato a um grupo", + "messages.profileSummary.goToProfile": "Ir para o perfil", + "messages.profileSummary.goToProfileAriaLabel": "ir para o perfil", + "messages.sendMessage.errorToast": "Não foi possível enviar sua última mensagem. Tente novamente.", + "messages.shared.addMember": "Adicionar membro", + "messages.shared.archiveChat": "Arquivar conversa", + "messages.shared.contactDetails": "Detalhes do contato", + "messages.shared.deletedUser": "Usuário excluído", + "messages.shared.leaveGroup": "Sair do grupo", + "messages.shared.loadingMoreProfiles": "carregando mais perfis", + "messages.shared.memberCount": "{count, plural, one {# membro} other {# membros}}", + "messages.shared.members": "Membros", + "messages.shared.remove": "Remover", + "messages.shared.returnToChatRooms": "voltar para as conversas existentes", + "messages.shared.seeProfile": "Ver perfil", + "messages.shared.somethingWentWrong": "Algo deu errado", + "messages.shared.unarchiveChat": "Desarquivar conversa", + "messages.shared.yesterday": "Ontem", + "navigations.accountMenu.login": "Entrar", + "navigations.accountMenu.register": "Cadastrar-se", + "navigations.accountPopover.avatarAlt": "Avatar do usuário", + "navigations.accountPopover.logout": "Sair", + "navigations.verticalFooter.avatarAlt": "Avatar do perfil", + "notifications.commentCreated.message": "comentou {target, select, comment {no seu comentário} contentpost {na sua publicação} file {no seu arquivo} page {na sua página} other {no seu {target}}}", + "notifications.commentReply.message": "respondeu ao seu comentário", + "notifications.emptyState.description": "Suas próximas notificações aparecerão aqui.", + "notifications.emptyState.title": "Você não tem notificações.", + "notifications.item.avatarAlt": "Avatar do usuário da notificação", + "notifications.list.closeAriaLabel": "fechar notificações", + "notifications.list.markAllAsRead": "Marcar todas como lidas", + "notifications.list.older": "Anteriores", + "notifications.list.title": "Notificações", + "notifications.popover.ariaLabel": "ver notificações", + "notifications.popover.label": "Notificações", + "notifications.reactionCreated.message": "curtiu {target, select, comment {seu comentário} contentpost {sua publicação} file {seu arquivo} page {sua página} other {seu {target}}}", + "payments.addCard.addressMissing": "O elemento de endereço está ausente. Tente novamente.", + "payments.addCard.billingAddress": "Endereço de cobrança", + "payments.addCard.cardInformation": "Informações do cartão", + "payments.addCard.confirmError": "Erro ao confirmar o cartão: {message}", + "payments.addCard.genericError": "Erro ao confirmar o cartão:", + "payments.addCard.incompleteAddress": "Erro ao confirmar o cartão: endereço incompleto", + "payments.addCard.success": "Cartão adicionado com sucesso", + "payments.addCard.unknownError": "Erro desconhecido", + "payments.addPaymentMethod": "Adicionar forma de pagamento", + "payments.card.expired": "Expirado", + "payments.card.expiredOn": "Expirou: {month}/{year}", + "payments.card.expires": "Expira: {month}/{year}", + "payments.card.fallbackName": "Cartão", + "payments.checkout.address": "Endereço", + "payments.checkout.addressHelper": "Usado para calcular os impostos.", + "payments.checkout.consent": "Ao concluir sua compra, você autoriza o BaseApp a armazenar sua forma de pagamento para cobranças futuras. Você pode alterar sua forma de pagamento a qualquer momento nas configurações da sua conta.", + "payments.checkout.createFailed": "Falha ao criar a assinatura: {message}", + "payments.checkout.paymentConfirmationFailed": "Falha na confirmação do pagamento: {message}", + "payments.checkout.placeOrder": "Fazer pedido", + "payments.checkout.retry": "Tentar novamente", + "payments.checkout.taxesPerMonth": "+ impostos /mês", + "payments.checkout.unexpectedError": "Ocorreu um erro inesperado. Tente novamente.", + "payments.checkout.updateFailed": "Falha ao atualizar a assinatura", + "payments.confirmation.description": "Obrigado pela sua assinatura! Acesse os detalhes do plano clicando no botão abaixo", + "payments.confirmation.descriptionWithEmail": "Obrigado pela sua assinatura! Acesse os detalhes do plano clicando no botão abaixo ou no e-mail que acabamos de enviar para.", + "payments.confirmation.orderNumber": "Número do pedido:", + "payments.confirmation.planDetails": "Detalhes do plano", + "payments.confirmation.title": "Assinatura realizada com sucesso", + "payments.errors.tryAgain": "Tente novamente.", + "payments.freePlan.description": "Aproveite o acesso essencial à plataforma sem custo. Perfeito para explorar os recursos básicos e começar.", + "payments.freePlan.features.coreFeatures": "Acesso aos recursos principais", + "payments.freePlan.features.limitedStorage": "Espaço de armazenamento limitado", + "payments.freePlan.features.noCommitment": "Sem compromisso", + "payments.freePlan.features.standardSupport": "Suporte padrão", + "payments.freePlan.title": "Grátis", + "payments.invoices.empty": "Nenhum recibo encontrado", + "payments.invoices.header.amount": "Valor", + "payments.invoices.header.date": "Data", + "payments.invoices.header.description": "Descrição", + "payments.invoices.header.details": "Detalhes", + "payments.invoices.header.info": "Informações", + "payments.invoices.header.status": "Status", + "payments.invoices.receipt": "Recibo", + "payments.invoices.status.draft": "rascunho", + "payments.invoices.status.failed": "falhou", + "payments.invoices.status.open": "em aberto", + "payments.invoices.status.paid": "pago", + "payments.invoices.status.pending": "pendente", + "payments.invoices.status.uncollectible": "incobrável", + "payments.invoices.status.void": "anulado", + "payments.invoices.title": "Recibos da assinatura", + "payments.payment": "Pagamento", + "payments.paymentDropdown.selectPaymentMethod": "Selecione a forma de pagamento", + "payments.paymentMethods.cardFallback": "CARTÃO", + "payments.paymentMethods.default": "Padrão", + "payments.paymentMethods.description": "Adicione e gerencie suas formas de pagamento com o Stripe.", + "payments.paymentMethods.remove": "Remover", + "payments.paymentMethods.removeDialog.content": "Tem certeza de que deseja remover esta forma de pagamento? Se for sua única forma de pagamento, seus pagamentos serão interrompidos até que uma nova seja adicionada.", + "payments.paymentMethods.removeDialog.title": "Remover forma de pagamento", + "payments.paymentMethods.removeFailed": "Falha ao excluir a forma de pagamento: {message}", + "payments.paymentMethods.removeSuccess": "Forma de pagamento removida com sucesso", + "payments.paymentMethods.removing": "Removendo...", + "payments.paymentMethods.setAsDefault": "Definir como padrão", + "payments.paymentMethods.setDefaultFailed": "Falha ao definir a forma de pagamento padrão: {message}", + "payments.paymentMethods.setDefaultSuccess": "Forma de pagamento padrão definida com sucesso", + "payments.paymentMethods.title": "Formas de pagamento", + "payments.plans.manageSubscription": "Gerenciar assinatura", + "payments.plans.monthly": "Mensal", + "payments.plans.perMonth": "USD/mês", + "payments.plans.perYear": "USD/ano", + "payments.plans.subscribe": "Assinar", + "payments.plans.yearly": "Anual", + "payments.status.active": "Ativa", + "payments.stripe.cancelSubscriptionFailed": "Falha ao cancelar a assinatura", + "payments.stripe.cancelSubscriptionSuccess": "Assinatura cancelada com sucesso.", + "payments.stripe.confirmCardPaymentFailed": "Falha ao confirmar o pagamento com cartão: {message}", + "payments.stripe.confirmCardPaymentFailedFallback": "Falha ao confirmar o pagamento com cartão.", + "payments.subscription.cancel": "Cancelar assinatura", + "payments.subscription.cancelDialog.back": "Voltar", + "payments.subscription.cancelDialog.content": "Tem certeza de que deseja cancelar sua assinatura? Você manterá o acesso ao seu plano atual até o fim do período de cobrança. Depois disso, sua conta passará para o plano gratuito.", + "payments.subscription.changePlan": "Alterar plano", + "payments.subscription.nextBill": "Sua próxima fatura é de {amount} em {date}", + "payments.subscription.nextBillFree": "Sua próxima fatura é gratuita em {date}", + "payments.subscription.planFallback": "Plano de assinatura", + "payments.subscription.status.canceled": "Cancelada", + "payments.subscription.status.pastDue": "Em atraso", + "payments.subscription.status.pending": "Pendente", + "payments.subscription.status.unpaid": "Não paga", + "payments.subscription.title": "Assinatura", + "payments.subscription.updatePaymentMethodFailed": "Falha ao atualizar a forma de pagamento", + "payments.subscription.updated": "Assinatura atualizada com sucesso.", + "profiles.blockDialog.block.action": "Bloquear", + "profiles.blockDialog.block.content": "Essa pessoa não poderá enviar mensagens para você nem encontrar seu perfil ou conteúdo. Ela não verá seus comentários e reações, e não poderá mencionar ou seguir seu perfil. Não vamos notificá-la de que foi bloqueada.", + "profiles.blockDialog.block.title": "Bloquear {name}?", + "profiles.blockDialog.blockProfile": "Bloquear perfil", + "profiles.blockDialog.blockedToast": "{name} foi bloqueado", + "profiles.blockDialog.unblock.action": "Desbloquear", + "profiles.blockDialog.unblock.content": "Essa pessoa poderá enviar mensagens para você e encontrar seu perfil ou conteúdo. Ela verá seus comentários e reações, e poderá mencionar ou seguir seu perfil. Não vamos notificá-la de que foi desbloqueada.", + "profiles.blockDialog.unblock.title": "Desbloquear {name}?", + "profiles.blockDialog.unblockProfile": "Desbloquear perfil", + "profiles.blockDialog.unblockedToast": "{name} foi desbloqueado", + "profiles.createProfile.description": "Crie uma organização e convide vários membros para gerenciar e colaborar.", + "profiles.createProfile.errors.createFailed": "Não foi possível criar a organização. Tente novamente ou entre em contato com o suporte se o problema persistir.", + "profiles.createProfile.errors.nameTaken": "Já existe uma organização com esse nome. Escolha um nome diferente.", + "profiles.createProfile.errors.urlPathInvalid": "Formato de caminho de URL inválido. Use apenas letras minúsculas, números e hifens.", + "profiles.createProfile.errors.urlPathTaken": "Esse caminho de URL já está em uso. Escolha outro.", + "profiles.createProfile.name": "Nome", + "profiles.createProfile.submit": "Criar organização", + "profiles.createProfile.terms": "Ao confirmar, você concorda com nossos Termos e Condições.", + "profiles.createProfile.title": "Nova organização", + "profiles.createProfile.urlPath": "Caminho de URL", + "profiles.createProfile.validation.nameMax": "Deve ter no máximo 255 caracteres", + "profiles.createProfile.validation.nameMin": "Deve ter pelo menos três caracteres", + "profiles.createProfile.validation.urlPathFormat": "Deve conter apenas letras minúsculas, números e hifens", + "profiles.createProfile.validation.urlPathMax": "Deve ter no máximo 500 caracteres", + "profiles.currentProfile.avatarAlt": "Avatar do perfil atual", + "profiles.followToggle.follow": "Seguir", + "profiles.inviteMemberDialog.addMembersFailed": "Falha ao adicionar membros", + "profiles.inviteMemberDialog.description": "Adicione perfis à sua organização ou envie um convite por e-mail.", + "profiles.inviteMemberDialog.membersAdded": "{count, plural, one {Membro adicionado} other {# membros adicionados}}", + "profiles.inviteMemberDialog.removeMember": "Remover membro", + "profiles.inviteMemberDialog.searchPlaceholder": "Convide membros por nome ou e-mail", + "profiles.inviteMemberDialog.sendInvitationsFailed": "Falha ao enviar os convites", + "profiles.inviteMemberDialog.submit": "Convidar", + "profiles.inviteMemberDialog.title": "Adicionar membro", + "profiles.members.actions.remove": "Remover", + "profiles.members.actions.resendInvitation": "Reenviar convite", + "profiles.members.addMember": "Adicionar membro", + "profiles.members.avatarAlt": "Avatar do perfil", + "profiles.members.cancelInvitation.failed": "Não foi possível remover o convite", + "profiles.members.cancelInvitation.success": "Convite removido", + "profiles.members.changeRoleDialog.content": "Tem certeza de que deseja promover este membro a administrador? Ele terá direitos administrativos completos, incluindo a capacidade de gerenciar membros e configurações.", + "profiles.members.changeRoleDialog.title": "Alterar permissões do usuário?", + "profiles.members.count": "{count, plural, one {# membro} other {# membros}}", + "profiles.members.dialogs.back": "Voltar", + "profiles.members.removeInvitationDialog.content": "Tem certeza de que deseja remover este convite? Ele será excluído da lista.", + "profiles.members.removeInvitationDialog.title": "Remover convite", + "profiles.members.removeMember.success": "Membro removido com sucesso", + "profiles.members.removeMemberDialog.content": "Tem certeza de que deseja remover este membro? Esta ação revogará o acesso dele ao perfil da organização.", + "profiles.members.removeMemberDialog.title": "Remover membro", + "profiles.members.resendInvitation.emailNotSent": "Convite atualizado, mas não foi possível enviar o e-mail", + "profiles.members.resendInvitation.success": "Convite reenviado com sucesso", + "profiles.members.roles.admin": "Administrador", + "profiles.members.roles.manager": "Gerente", + "profiles.members.roles.owner": "Proprietário", + "profiles.members.statuses.active": "Ativo", + "profiles.members.statuses.declined": "Recusado", + "profiles.members.statuses.expired": "Expirado", + "profiles.members.statuses.inactive": "Inativo", + "profiles.members.statuses.pending": "Pendente", + "profiles.members.title": "Membros", + "profiles.permissionDenied.description": "Você não tem permissão para acessar esta página. Entre em contato com um administrador se acreditar que isso é um erro.", + "profiles.permissionDenied.goBack": "Voltar", + "profiles.permissionDenied.title": "Permissão necessária", + "profiles.profile.bannerAlt": "Banner inicial", + "profiles.profile.editProfile": "Editar perfil", + "profiles.profile.followers": "Seguidores", + "profiles.profile.following": "Seguindo", + "profiles.profile.shareCopied": "URL do perfil copiada para a área de transferência!", + "profiles.profile.shareProfile": "Compartilhar perfil", + "profiles.profilesList.ariaLabel": "Lista de perfis disponíveis", + "profiles.profilesList.empty.description": "Seus próximos perfis aparecerão aqui.", + "profiles.profilesList.empty.title": "Não há perfis criados.", + "profiles.profilesList.switchTo": "Alternar para {name}", + "profiles.profilesList.switchToThisProfile": "Alternar para este perfil", + "profiles.profilesList.switchedTo": "Você alternou para {name}", + "profiles.report.alreadyReported": "Você já denunciou este perfil.", + "profiles.report.anonymousNotice": "Sua denúncia é anônima. Se alguém estiver em perigo imediato, ligue para os serviços de emergência locais - não espere.", + "profiles.report.confirmation.title": "Obrigado por denunciar {reportType}", + "profiles.report.menuItem": "Denunciar perfil", + "profiles.report.summary.aboutProblem": "Sobre o problema", + "profiles.report.summary.subTypeQuestion": "Que tipo de {reportType}?", + "profiles.report.summary.submit": "Enviar denúncia", + "profiles.report.summary.title": "Você está prestes a enviar uma denúncia", + "profiles.report.text.description": "Use o campo de texto abaixo para explicar qual é o problema que você está denunciando.", + "profiles.report.text.placeholder": "Considero a publicação ofensiva...", + "profiles.report.text.title": "Como você descreveria o problema?", + "profiles.report.title": "Denunciar", + "profiles.report.whyReporting": "Por que você está denunciando isto?", + "profiles.settings.avatarAlt": "Imagem do avatar", + "profiles.settings.bio": "Biografia", + "profiles.settings.changeBanner": "Alterar banner", + "profiles.settings.changeImage": "Alterar imagem", + "profiles.settings.name": "Nome", + "profiles.settings.phoneNumber": "Número de telefone", + "profiles.settings.remove": "Remover", + "profiles.settings.removeAvatarAriaLabel": "Botão para remover o avatar", + "profiles.settings.removeBannerAriaLabel": "Botão para remover o banner", + "profiles.settings.saveChanges": "Salvar alterações", + "profiles.settings.subtitle": "Gerencie as informações pessoais que você e outras pessoas veem.", + "profiles.settings.title": "Perfil", + "profiles.settings.updateError": "Algo deu errado", + "profiles.settings.updateSuccess": "Perfil atualizado", + "profiles.settings.uploadBanner": "Enviar banner", + "profiles.settings.uploadImage": "Enviar imagem", + "profiles.settings.username": "Nome de usuário", + "profiles.settings.validation.nameEmpty": "Insira um nome.", + "profiles.settings.validation.urlPathEmpty": "O nome de usuário deve ter pelo menos 8 caracteres.", + "profiles.settings.validation.urlPathInvalid": "O nome de usuário só pode conter letras e números", + "profiles.switchProfileMenu.label": "Alternar perfil", + "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/activity-log/web/ActivityLogComponent/DateFilterChip/index.tsx b/packages/components/modules/activity-log/web/ActivityLogComponent/DateFilterChip/index.tsx index 372497128..c432f2950 100644 --- a/packages/components/modules/activity-log/web/ActivityLogComponent/DateFilterChip/index.tsx +++ b/packages/components/modules/activity-log/web/ActivityLogComponent/DateFilterChip/index.tsx @@ -6,11 +6,13 @@ import { DATE_FORMAT, formatDate } from '@baseapp-frontend/utils' import { KeyboardArrowDown } from '@mui/icons-material' import { Box, Chip, Divider, Menu, Theme, Typography, useMediaQuery } from '@mui/material' import { DateTime } from 'luxon' +import { useIntl } from 'react-intl' import DateFilterComponent from '../DateFilterComponent' import { DateFilterChipProps } from './types' const DateFilterChip: FC = ({ fetchParameters, executeRefetch }) => { + const intl = useIntl() const [drawerOpen, setDrawerOpen] = useState(false) const [anchorEl, setAnchorEl] = useState(null) const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm')) @@ -33,9 +35,17 @@ const DateFilterChip: FC = ({ fetchParameters, executeRefet const toDate = formatLabelDate(createdTo) if (createdFrom && createdTo) return `${fromDate} - ${toDate}` - if (createdFrom) return `From ${fromDate}` - if (createdTo) return `Until ${toDate}` - return 'Period' + if (createdFrom) + return intl.formatMessage( + { id: 'activityLog.dateFilter.from', defaultMessage: 'From {date}' }, + { date: fromDate }, + ) + if (createdTo) + return intl.formatMessage( + { id: 'activityLog.dateFilter.until', defaultMessage: 'Until {date}' }, + { date: toDate }, + ) + return intl.formatMessage({ id: 'activityLog.dateFilter.period', defaultMessage: 'Period' }) } const handleOnClickOnChip = (event: React.MouseEvent) => { @@ -49,7 +59,7 @@ const DateFilterChip: FC = ({ fetchParameters, executeRefet const renderDateFilterComponent = (onClose: () => void) => ( <> - Period + {intl.formatMessage({ id: 'activityLog.dateFilter.period', defaultMessage: 'Period' })} = ({ onApply, onClearFilter, }) => { + const intl = useIntl() const [tempCreatedFrom, setTempCreatedFrom] = useState(createdFrom ?? null) const [tempCreatedTo, setTempCreatedTo] = useState(createdTo ?? null) @@ -23,13 +25,19 @@ const DateFilterComponent: FC = ({ const errorMessage = React.useMemo(() => { switch (error) { case 'minDate': - return 'End date cannot be earlier than start date.' + return intl.formatMessage({ + id: 'activityLog.dateFilter.error.minDate', + defaultMessage: 'End date cannot be earlier than start date.', + }) case 'invalidDate': - return 'Your date is not valid' + return intl.formatMessage({ + id: 'activityLog.dateFilter.error.invalidDate', + defaultMessage: 'Your date is not valid', + }) default: return '' } - }, [error]) + }, [error, intl]) const handleApply = () => { if (tempCreatedTo && tempCreatedFrom && tempCreatedTo < tempCreatedFrom) { @@ -65,7 +73,10 @@ const DateFilterComponent: FC = ({ setTempCreatedFrom(newValue)} disableFuture value={tempCreatedFrom} @@ -81,7 +92,10 @@ const DateFilterComponent: FC = ({ }} /> setTempCreatedTo(newValue)} onError={(newError) => setError(newError)} @@ -103,10 +117,13 @@ const DateFilterComponent: FC = ({ diff --git a/packages/components/modules/activity-log/web/ActivityLogComponent/EventFilterChip/index.tsx b/packages/components/modules/activity-log/web/ActivityLogComponent/EventFilterChip/index.tsx index 568d9e6a5..90bcfb56f 100644 --- a/packages/components/modules/activity-log/web/ActivityLogComponent/EventFilterChip/index.tsx +++ b/packages/components/modules/activity-log/web/ActivityLogComponent/EventFilterChip/index.tsx @@ -1,11 +1,14 @@ import { FC, MouseEvent, useState } from 'react' import { Checkbox, Chip, ListItemText, Menu, MenuItem } from '@mui/material' +import { useIntl } from 'react-intl' +import { EVENT_FILTER_OPTION_MESSAGES } from '../constants' import { EventFilterOption } from '../types' import { EventFilterChipProps } from './types' const EventFilterChip: FC = ({ options, selectedOptions, onChange }) => { + const intl = useIntl() const [anchorEl, setAnchorEl] = useState(null) const handleClick = (event: MouseEvent) => { @@ -31,12 +34,18 @@ const EventFilterChip: FC = ({ options, selectedOptions, o return ( <> - + {options.map((option) => ( handleToggle(option)}> -1} /> - + ))} diff --git a/packages/components/modules/activity-log/web/ActivityLogComponent/LogGroups/index.tsx b/packages/components/modules/activity-log/web/ActivityLogComponent/LogGroups/index.tsx index 8ad55ee6e..3594e5a71 100644 --- a/packages/components/modules/activity-log/web/ActivityLogComponent/LogGroups/index.tsx +++ b/packages/components/modules/activity-log/web/ActivityLogComponent/LogGroups/index.tsx @@ -3,6 +3,7 @@ import { FC } from 'react' import { AvatarDeletedUserIcon } from '@baseapp-frontend/design-system/components/web/icons' import { Box, CircularProgress, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { Virtuoso } from 'react-virtuoso' import { Timestamp } from '../../../../__shared__/web' @@ -20,6 +21,7 @@ const LogGroups: FC = ({ hasNext, isLoadingNext, }) => { + const intl = useIntl() const renderLogItem = (log: ActivityLogNode, isLast: boolean) => { if (!log) return null @@ -33,13 +35,20 @@ const LogGroups: FC = ({ ) } const renderUserName = (group: LogGroup) => { - if (group.logs[0]?.user == null) return 'Deleted User' + if (group.logs[0]?.user == null) + return intl.formatMessage({ + id: 'activityLog.logGroups.deletedUser', + defaultMessage: 'Deleted User', + }) return group.logs[0]?.user?.fullName } const renderAvatar = (group: LogGroup) => { diff --git a/packages/components/modules/activity-log/web/ActivityLogComponent/LogItem/constants.ts b/packages/components/modules/activity-log/web/ActivityLogComponent/LogItem/constants.ts new file mode 100644 index 000000000..1a5d7482c --- /dev/null +++ b/packages/components/modules/activity-log/web/ActivityLogComponent/LogItem/constants.ts @@ -0,0 +1,28 @@ +import { defineMessages } from 'react-intl' + +export const VERB_MESSAGES = defineMessages({ + 'comments.add_comment': { + id: 'activityLog.logItem.verb.addComment', + defaultMessage: 'Created a comment', + }, + 'comments.change_comment': { + id: 'activityLog.logItem.verb.changeComment', + defaultMessage: 'Edited a comment', + }, + 'comments.delete_comment': { + id: 'activityLog.logItem.verb.deleteComment', + defaultMessage: 'Deleted a comment', + }, + 'comments.reply_comment': { + id: 'activityLog.logItem.verb.replyComment', + defaultMessage: 'Replied to a comment', + }, + 'comments.pin_comment': { + id: 'activityLog.logItem.verb.pinComment', + defaultMessage: 'Pinned a comment', + }, + 'baseapp_reactions.add_reaction': { + id: 'activityLog.logItem.verb.addReaction', + defaultMessage: 'Added a reaction', + }, +}) diff --git a/packages/components/modules/activity-log/web/ActivityLogComponent/LogItem/index.tsx b/packages/components/modules/activity-log/web/ActivityLogComponent/LogItem/index.tsx index d186de373..742eae5c7 100644 --- a/packages/components/modules/activity-log/web/ActivityLogComponent/LogItem/index.tsx +++ b/packages/components/modules/activity-log/web/ActivityLogComponent/LogItem/index.tsx @@ -1,26 +1,21 @@ import { FC } from 'react' import { Box, Typography, useTheme } from '@mui/material' +import { useIntl } from 'react-intl' import { getUpdateMessage } from '../utils' +import { VERB_MESSAGES } from './constants' import { LogDiff, LogItemProps } from './types' -const verbMapping: { [key: string]: string } = { - 'comments.add_comment': 'Created a comment', - 'comments.change_comment': 'Edited a comment', - 'comments.delete_comment': 'Deleted a comment', - 'comments.reply_comment': 'Replied to a comment', - 'comments.pin_comment': 'Pinned a comment', - 'baseapp_reactions.add_reaction': 'Added a reaction', -} - const LogItem: FC = ({ log, sx }) => { const theme = useTheme() + const intl = useIntl() if (!log?.verb) return null const getLogMessage = (verb: string, diff?: LogDiff) => { - if (diff) return getUpdateMessage(verb, diff) - return verbMapping[verb] ?? verb + if (diff) return getUpdateMessage(verb, diff, intl) + const message = VERB_MESSAGES[verb as keyof typeof VERB_MESSAGES] + return message ? intl.formatMessage(message) : verb } const diff = log.events?.edges?.[0]?.node?.diff ?? null diff --git a/packages/components/modules/activity-log/web/ActivityLogComponent/constants.ts b/packages/components/modules/activity-log/web/ActivityLogComponent/constants.ts index 7d1c00045..e51377d39 100644 --- a/packages/components/modules/activity-log/web/ActivityLogComponent/constants.ts +++ b/packages/components/modules/activity-log/web/ActivityLogComponent/constants.ts @@ -1 +1,41 @@ +import { defineMessages } from 'react-intl' + export const EVENT_FILTER_OPTIONS = ['All', 'Comments', 'Reactions', 'Posts'] as const + +export const EVENT_FILTER_OPTION_MESSAGES = defineMessages({ + All: { + id: 'activityLog.eventFilter.option.all', + defaultMessage: 'All', + }, + Comments: { + id: 'activityLog.eventFilter.option.comments', + defaultMessage: 'Comments', + }, + Reactions: { + id: 'activityLog.eventFilter.option.reactions', + defaultMessage: 'Reactions', + }, + Posts: { + id: 'activityLog.eventFilter.option.posts', + defaultMessage: 'Posts', + }, +}) + +export const UPDATE_MESSAGES = defineMessages({ + updated: { + id: 'activityLog.logItem.updated', + defaultMessage: 'Updated {name}', + }, + profilePicture: { + id: 'activityLog.logItem.updatedProfilePicture', + defaultMessage: 'Updated their profile picture', + }, + profileBanner: { + id: 'activityLog.logItem.updatedProfileBanner', + defaultMessage: 'Updated their profile banner', + }, + bio: { + id: 'activityLog.logItem.updatedBio', + defaultMessage: 'Updated their bio', + }, +}) diff --git a/packages/components/modules/activity-log/web/ActivityLogComponent/index.tsx b/packages/components/modules/activity-log/web/ActivityLogComponent/index.tsx index cdf1c3ab6..152b53213 100644 --- a/packages/components/modules/activity-log/web/ActivityLogComponent/index.tsx +++ b/packages/components/modules/activity-log/web/ActivityLogComponent/index.tsx @@ -6,6 +6,7 @@ import { Searchbar } from '@baseapp-frontend/design-system/components/web/inputs import { Box, Typography } from '@mui/material' import { useForm } from 'react-hook-form' +import { useIntl } from 'react-intl' import { SearchNotFoundState } from '../../../__shared__/web' import { FetchParameters, useActivityLogs } from '../../common' @@ -19,6 +20,7 @@ const ActivityLogComponent: FC = ({ LogGroups = DefaultLogGroups, LogGroupsProps, }) => { + const intl = useIntl() const [fetchParameters, setFetchParameters] = useState({ createdFrom: null, createdTo: null, @@ -60,10 +62,13 @@ const ActivityLogComponent: FC = ({ - Activity Log + {intl.formatMessage({ id: 'activityLog.title', defaultMessage: 'Activity Log' })} = ({ {!isPending && (fetchParameters.createdFrom != null || fetchParameters.createdTo != null) && emptyLogsList && ( - + )} ): string => { - const baseMessage = `Updated ${verb.split('.')[0]}` +import type { IntlShape } from 'react-intl' + +import { UPDATE_MESSAGES } from './constants' + +export const getUpdateMessage = ( + verb: string, + diff: Record, + intl: IntlShape, +): string => { + const baseMessage = intl.formatMessage(UPDATE_MESSAGES.updated, { name: verb.split('.')[0] }) if (!diff) { return baseMessage @@ -8,13 +16,13 @@ export const getUpdateMessage = (verb: string, diff: Record): strin const updateMessages: string[] = Object.keys(diff).map((key) => { switch (key) { case 'image': - return 'Updated their profile picture' + return intl.formatMessage(UPDATE_MESSAGES.profilePicture) case 'banner_image': - return 'Updated their profile banner' + return intl.formatMessage(UPDATE_MESSAGES.profileBanner) case 'biography': - return 'Updated their bio' + return intl.formatMessage(UPDATE_MESSAGES.bio) default: - return `Updated ${key.replace('_', ' ')}` + return intl.formatMessage(UPDATE_MESSAGES.updated, { name: key.replace('_', ' ') }) } }) 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 e091233c5..3d57439b5 100644 --- a/packages/components/modules/comments/web/CommentCreate/index.tsx +++ b/packages/components/modules/comments/web/CommentCreate/index.tsx @@ -151,7 +151,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/CommentUpdate/CommentFilesUpsertActions/index.tsx b/packages/components/modules/comments/web/CommentUpdate/CommentFilesUpsertActions/index.tsx index e8d927b44..832c0d676 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' @@ -20,22 +22,32 @@ const ACCEPTED_FILE_TYPES = { * 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} - maxFileSize={MAX_FILE_SIZE} - acceptedFileTypes={ACCEPTED_FILE_TYPES} - autoAttach - /> - - - - -) +const CommentFilesUpsertActions: FC = ({ target }) => { + const intl = useIntl() + + return ( + + } + maxFiles={MAX_FILES} + maxFileSize={MAX_FILE_SIZE} + acceptedFileTypes={ACCEPTED_FILE_TYPES} + autoAttach + /> + + + + + ) +} export default CommentFilesUpsertActions export type { CommentFilesUpsertActionsProps } from './types' diff --git a/packages/components/modules/content-feed/web/ContentFeed/index.tsx b/packages/components/modules/content-feed/web/ContentFeed/index.tsx index 5180dadbe..c20299668 100644 --- a/packages/components/modules/content-feed/web/ContentFeed/index.tsx +++ b/packages/components/modules/content-feed/web/ContentFeed/index.tsx @@ -4,6 +4,7 @@ import { FC, useCallback } from 'react' import { Button, Typography } from '@mui/material' import { useRouter } from 'next/navigation' +import { useIntl } from 'react-intl' import PostList from '../PostList' import { HeaderContainer, RootContainer } from '../styled' @@ -11,6 +12,7 @@ import { ContentFeedProps } from './types' const ContentFeed: FC = ({ preloadedQuery }) => { const router = useRouter() + const intl = useIntl() const onNewPost = useCallback(() => { router.push('/posts/new') @@ -20,7 +22,7 @@ const ContentFeed: FC = ({ preloadedQuery }) => { - Content Feed + {intl.formatMessage({ id: 'contentFeed.title', defaultMessage: 'Content Feed' })} diff --git a/packages/components/modules/content-feed/web/PostCreate/index.tsx b/packages/components/modules/content-feed/web/PostCreate/index.tsx index 98303dcb2..59f707043 100644 --- a/packages/components/modules/content-feed/web/PostCreate/index.tsx +++ b/packages/components/modules/content-feed/web/PostCreate/index.tsx @@ -7,6 +7,7 @@ import { setFormRelayErrors, useNotification } from '@baseapp-frontend/utils' import { zodResolver } from '@hookform/resolvers/zod' import { useRouter } from 'next/navigation' import { useForm } from 'react-hook-form' +import { useIntl } from 'react-intl' import { useContentPostCreateMutation } from '../../common' import PostForm from '../PostForm' @@ -18,6 +19,7 @@ import { ContentPostCreateForm, UploadableContentPostFiles } from '../PostForm/t const PostCreate: FC = () => { const router = useRouter() + const intl = useIntl() const { sendToast } = useNotification() const [commitMutation, isMutationInFlight] = useContentPostCreateMutation() @@ -47,11 +49,23 @@ const PostCreate: FC = () => { onCompleted(response) { const errors = response.contentPostCreate?.errors if (errors) { - sendToast('Something went wrong', { type: 'error' }) + sendToast( + intl.formatMessage({ + id: 'contentFeed.postCreate.error', + defaultMessage: 'Something went wrong', + }), + { type: 'error' }, + ) setFormRelayErrors(form, errors) } else { form.reset({ content: '', isReactionsEnabled: true }) - sendToast('Post Created Successfully', { type: 'success' }) + sendToast( + intl.formatMessage({ + id: 'contentFeed.postCreate.success', + defaultMessage: 'Post Created Successfully', + }), + { type: 'success' }, + ) router.push(`/posts/${response.contentPostCreate?.contentPost?.node?.id}`) } }, diff --git a/packages/components/modules/content-feed/web/PostFooter/index.tsx b/packages/components/modules/content-feed/web/PostFooter/index.tsx index 6d5b5799d..907c195e3 100644 --- a/packages/components/modules/content-feed/web/PostFooter/index.tsx +++ b/packages/components/modules/content-feed/web/PostFooter/index.tsx @@ -7,20 +7,36 @@ import { ReplyIcon, SharePostIcon } from '@baseapp-frontend/design-system/compon import { Circle as CircleIcon } from '@mui/icons-material' import { IconButton, Stack, Typography } from '@mui/material' import { DateTime } from 'luxon' +import { useIntl } from 'react-intl' import PostReactionButton from '../PostReactionButton' import { PostFooterProps } from './types' const PostFooter: FC = ({ post }) => { + const intl = useIntl() const created = DateTime.fromISO(post.created) return ( - + - + diff --git a/packages/components/modules/content-feed/web/PostForm/index.tsx b/packages/components/modules/content-feed/web/PostForm/index.tsx index 6982ed14d..76b9934ac 100644 --- a/packages/components/modules/content-feed/web/PostForm/index.tsx +++ b/packages/components/modules/content-feed/web/PostForm/index.tsx @@ -7,12 +7,14 @@ import { TextField } from '@baseapp-frontend/design-system/components/web/inputs import { LoadingButton } from '@mui/lab' import { Box, Button, FormControlLabel, Switch, Typography } from '@mui/material' import { Controller } from 'react-hook-form' +import { useIntl } from 'react-intl' import PostImageDropzone from '../PostImageDropzone' import { ButtonContainer, HeaderContainer, RootContainer } from '../styled' import { PostFormProps } from './types' const PostForm: FC = ({ form, onSubmit, onCancel, isSaving }) => { + const intl = useIntl() const isReactionsEnabled = form.watch('isReactionsEnabled') const { formState: { isDirty, isValid }, @@ -23,11 +25,11 @@ const PostForm: FC = ({ form, onSubmit, onCancel, isSaving }) =>
- New Post + {intl.formatMessage({ id: 'contentFeed.postForm.title', defaultMessage: 'New Post' })} = ({ form, onSubmit, onCancel, isSaving }) => disabled={!isDirty || !isValid || isSaving} sx={{ maxWidth: 'fit-content', justifySelf: 'end' }} > - Publish + {intl.formatMessage({ + id: 'contentFeed.postForm.publish', + defaultMessage: 'Publish', + })} @@ -45,7 +50,10 @@ const PostForm: FC = ({ form, onSubmit, onCancel, isSaving }) => = ({ form, onSubmit, onCancel, isSaving }) => } /> } - label="Disable Reactions to this post" + label={intl.formatMessage({ + id: 'contentFeed.postForm.disableReactions', + defaultMessage: 'Disable Reactions to this post', + })} /> )} /> diff --git a/packages/components/modules/content-feed/web/PostHeader/index.tsx b/packages/components/modules/content-feed/web/PostHeader/index.tsx index 57178999b..e8f33d160 100644 --- a/packages/components/modules/content-feed/web/PostHeader/index.tsx +++ b/packages/components/modules/content-feed/web/PostHeader/index.tsx @@ -4,6 +4,7 @@ import { FC } from 'react' import { MoreVert as MoreVertIcon } from '@mui/icons-material' import { Avatar, IconButton, Stack, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { ProfileItemFragment$key } from '../../../../__generated__/ProfileItemFragment.graphql' @@ -11,6 +12,7 @@ import { ProfileItemFragment } from '../../../profiles/common' import { PostHeaderProps } from './types' const PostHeader: FC = ({ post }) => { + const intl = useIntl() const profile = useFragment(ProfileItemFragment, post?.profile) if (!profile) return null @@ -27,7 +29,13 @@ const PostHeader: FC = ({ post }) => { - + diff --git a/packages/components/modules/content-feed/web/PostImageDropzone/index.tsx b/packages/components/modules/content-feed/web/PostImageDropzone/index.tsx index 70dd701c9..a79dc6298 100644 --- a/packages/components/modules/content-feed/web/PostImageDropzone/index.tsx +++ b/packages/components/modules/content-feed/web/PostImageDropzone/index.tsx @@ -6,6 +6,7 @@ import { Dropzone } from '@baseapp-frontend/design-system/components/web/dropzon import { Box, Typography } from '@mui/material' import Image from 'next/image' +import { FormattedMessage } from 'react-intl' import { PostImageDropzoneProps } from './types' @@ -57,7 +58,10 @@ const PostImageDropzone: FC = ({ form }) => { accept={{ 'image/png': ['.png'], 'image/gif': ['.gif'], 'image/jpeg': ['.jpg', '.jpeg'] }} title={ - Click to browse or drag and drop images and videos. + } onFileClick={(selectedFile, index) => { diff --git a/packages/components/modules/content-feed/web/PostImageSlide/index.tsx b/packages/components/modules/content-feed/web/PostImageSlide/index.tsx index 880eb3783..ba7475a1d 100644 --- a/packages/components/modules/content-feed/web/PostImageSlide/index.tsx +++ b/packages/components/modules/content-feed/web/PostImageSlide/index.tsx @@ -2,6 +2,7 @@ import { FC } from 'react' +import { useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { ContentPostImageFragment } from '../../common/graphql/fragments/ContentPostImage' @@ -9,11 +10,21 @@ import { ImageSlide } from './styled' import { PostImageSlideProps } from './types' const PostImageSlide: FC = ({ imagesRef }) => { + const intl = useIntl() const target = useFragment(ContentPostImageFragment, imagesRef) if (!target?.image) return null - return + return ( + + ) } export default PostImageSlide diff --git a/packages/components/modules/content-feed/web/PostList/index.tsx b/packages/components/modules/content-feed/web/PostList/index.tsx index f62b3a4b9..ce0e9db7f 100644 --- a/packages/components/modules/content-feed/web/PostList/index.tsx +++ b/packages/components/modules/content-feed/web/PostList/index.tsx @@ -5,6 +5,7 @@ import React, { FC } from 'react' import { LoadingState } from '@baseapp-frontend/design-system/components/web/displays' import { Box } from '@mui/material' +import { useIntl } from 'react-intl' import { Components, Virtuoso } from 'react-virtuoso' import { useContentPosts } from '../../common' @@ -19,6 +20,7 @@ const Scroller: Components['List'] = React.forwardRef(({ style, children }, ref) )) const PostList: FC = ({ preloadedQuery }) => { + const intl = useIntl() const { data: { contentPosts }, loadNext, @@ -38,7 +40,10 @@ const PostList: FC = ({ preloadedQuery }) => { ) } diff --git a/packages/components/modules/content-feed/web/PostReactionButton/index.tsx b/packages/components/modules/content-feed/web/PostReactionButton/index.tsx index 8a9055f3a..ca27740cb 100644 --- a/packages/components/modules/content-feed/web/PostReactionButton/index.tsx +++ b/packages/components/modules/content-feed/web/PostReactionButton/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 { PostReactionButtonProps } from './types' -const PostReactionButton: FC = ({ target: targetRef, onClick }) => ( - - {({ handleReaction, target }) => ( - - - {target?.myReaction?.id ? ( - - ) : ( - - )} - - - {target?.reactionsCount?.total} - - - )} - -) +const PostReactionButton: FC = ({ target: targetRef, onClick }) => { + const intl = useIntl() + + return ( + + {({ handleReaction, target }) => ( + + + {target?.myReaction?.id ? ( + + ) : ( + + )} + + + {target?.reactionsCount?.total} + + + )} + + ) +} export default PostReactionButton diff --git a/packages/components/modules/files/web/AttachedFileItem/index.tsx b/packages/components/modules/files/web/AttachedFileItem/index.tsx index 745f76f37..29bc372f2 100644 --- a/packages/components/modules/files/web/AttachedFileItem/index.tsx +++ b/packages/components/modules/files/web/AttachedFileItem/index.tsx @@ -11,6 +11,7 @@ import { InsertDriveFile as FileIcon, } from '@mui/icons-material' import { Box, Card, CardContent, Chip, Stack, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { FileItemFragment } from '../../common/graphql/fragments/FileItem' @@ -32,6 +33,7 @@ const AttachedFileItem: FC = ({ variant = 'card', editable = false, }) => { + const intl = useIntl() const file = useFragment(FileItemFragment, fileRef) const { handleDelete, isDeletingFile } = useFileDeleteLogic({ targetObjectId }) @@ -68,7 +70,10 @@ const AttachedFileItem: FC = ({ } name={file.fileName} @@ -84,8 +89,11 @@ const AttachedFileItem: FC = ({ onClick={() => handleDelete(file.id)} disabled={isDeletingFile} isLoading={isDeletingFile} - title="Remove" - aria-label="Remove" + title={intl.formatMessage({ id: 'files.actions.remove', defaultMessage: 'Remove' })} + aria-label={intl.formatMessage({ + id: 'files.actions.remove', + defaultMessage: 'Remove', + })} > @@ -93,8 +101,14 @@ const AttachedFileItem: FC = ({ handleDownload(file.url)} - title="Download" - aria-label="Download" + title={intl.formatMessage({ + id: 'files.actions.download', + defaultMessage: 'Download', + })} + aria-label={intl.formatMessage({ + id: 'files.actions.download', + defaultMessage: 'Download', + })} > @@ -115,7 +129,13 @@ const AttachedFileItem: FC = ({ = ({ {file.createdBy && file.createdBy.fullName && ( - Uploaded by {file.createdBy.fullName} + {intl.formatMessage( + { + id: 'files.attachedFileItem.uploadedBy', + defaultMessage: 'Uploaded by {name}', + }, + { name: file.createdBy.fullName }, + )} )} - handleDownload(file.url)} title="Download"> + handleDownload(file.url)} + title={intl.formatMessage({ + id: 'files.actions.download', + defaultMessage: 'Download', + })} + > @@ -154,7 +187,7 @@ const AttachedFileItem: FC = ({ size="small" onClick={() => handleDelete(file.id)} color="error" - title="Delete" + title={intl.formatMessage({ id: 'common.delete', defaultMessage: 'Delete' })} disabled={isDeletingFile} isLoading={isDeletingFile} > @@ -165,7 +198,14 @@ const AttachedFileItem: FC = ({ {file.uploadStatus && file.uploadStatus !== 'COMPLETED' && ( - + )} {file.description && ( diff --git a/packages/components/modules/files/web/FileThumbnail/index.tsx b/packages/components/modules/files/web/FileThumbnail/index.tsx index 1b83c696a..b66d23628 100644 --- a/packages/components/modules/files/web/FileThumbnail/index.tsx +++ b/packages/components/modules/files/web/FileThumbnail/index.tsx @@ -9,6 +9,7 @@ import { Videocam as VideoIcon, } from '@mui/icons-material' import { Box } from '@mui/material' +import { useIntl } from 'react-intl' import { getFileType, isImageFile } from '../../common/utils/formatters' import type { FileThumbnailProps } from './types' @@ -31,6 +32,7 @@ const getTypeIcon = (contentType?: string | null) => { * preview when available, otherwise a primary-colored tile with a type icon. */ const FileThumbnail: FC = ({ src, contentType, alt, size = 40 }) => { + const intl = useIntl() const showImage = Boolean(src) && isImageFile(contentType) if (showImage) { @@ -38,7 +40,7 @@ const FileThumbnail: FC = ({ src, contentType, alt, size = 4 = ({ @@ -14,6 +16,7 @@ const FileUploadDropzone: FC = ({ acceptedFileTypes, disabled, }) => { + const intl = useIntl() const handleSelect = useCallback( (files: File | File[] | Blob | Blob[]) => { const fileArray = Array.isArray(files) ? files : [files] @@ -30,8 +33,14 @@ const FileUploadDropzone: FC = ({ multiple={maxFiles > 1} maxFileSize={maxFileSize / (1024 * 1024)} // Convert bytes to MB asBase64={false} // Keep as File objects for chunked upload - title="Upload Files" - subTitle={`Max ${maxFiles} files, ${Math.round(maxFileSize / (1024 * 1024))}MB each`} + title={intl.formatMessage({ id: 'files.dropzone.title', defaultMessage: 'Upload Files' })} + subTitle={intl.formatMessage( + { + id: 'files.dropzone.subtitle', + defaultMessage: 'Max {maxFiles} files, {maxFileSize}MB each', + }, + { maxFiles, maxFileSize: Math.round(maxFileSize / (1024 * 1024)) }, + )} includeActionButton={false} DropzoneOptions={{ disabled, diff --git a/packages/components/modules/files/web/FileUploadList/UploadingFileItem/FileProgress.tsx b/packages/components/modules/files/web/FileUploadList/UploadingFileItem/FileProgress.tsx index d14e0d6b8..e99ad912d 100644 --- a/packages/components/modules/files/web/FileUploadList/UploadingFileItem/FileProgress.tsx +++ b/packages/components/modules/files/web/FileUploadList/UploadingFileItem/FileProgress.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react' import { Box, LinearProgress, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { FileUploadStatus } from '../../../common/constants' @@ -12,6 +13,7 @@ interface FileProgressProps { } const FileProgress: FC = ({ progress, status }) => { + const intl = useIntl() const getColor = () => { switch (status) { case FileUploadStatus.UPLOADING: @@ -30,17 +32,20 @@ const FileProgress: FC = ({ progress, status }) => { const getStatusText = () => { switch (status) { case FileUploadStatus.PENDING: - return 'Pending...' + return intl.formatMessage({ id: 'files.progress.pending', defaultMessage: 'Pending...' }) case FileUploadStatus.UPLOADING: - return `Uploading ${Math.round(progress)}%` + return intl.formatMessage( + { id: 'files.progress.uploading', defaultMessage: 'Uploading {progress}%' }, + { progress: Math.round(progress) }, + ) case FileUploadStatus.PAUSED: - return 'Paused' + return intl.formatMessage({ id: 'files.progress.paused', defaultMessage: 'Paused' }) case FileUploadStatus.COMPLETED: - return 'Completed' + return intl.formatMessage({ id: 'files.progress.completed', defaultMessage: 'Completed' }) case FileUploadStatus.FAILED: - return 'Failed' + return intl.formatMessage({ id: 'files.progress.failed', defaultMessage: 'Failed' }) case FileUploadStatus.ABORTED: - return 'Aborted' + return intl.formatMessage({ id: 'files.progress.aborted', defaultMessage: 'Aborted' }) default: return '' } diff --git a/packages/components/modules/files/web/FileUploadList/UploadingFileItem/index.tsx b/packages/components/modules/files/web/FileUploadList/UploadingFileItem/index.tsx index 27148db5f..366701d7d 100644 --- a/packages/components/modules/files/web/FileUploadList/UploadingFileItem/index.tsx +++ b/packages/components/modules/files/web/FileUploadList/UploadingFileItem/index.tsx @@ -19,6 +19,7 @@ import { Stack, Typography, } from '@mui/material' +import { useIntl } from 'react-intl' import { FileUploadStatus } from '../../../common/constants' import { useFileUploadStore } from '../../../common/context/FileUploadProvider' @@ -35,6 +36,7 @@ const UploadingFileItem: FC = ({ allowRetry = true, variant = 'card', }) => { + const intl = useIntl() const removeFile = useFileUploadStore((state) => state.removeFile) const pauseFile = useFileUploadStore((state) => state.pauseFile) const { resumeUpload, retryUpload } = useChunkedUpload() @@ -89,22 +91,39 @@ const UploadingFileItem: FC = ({ const actions = ( {canPause && ( - + )} {canResume && ( - + )} {canRetry && ( - + )} {canRemove && ( - + )} diff --git a/packages/components/modules/files/web/FileUploadList/index.tsx b/packages/components/modules/files/web/FileUploadList/index.tsx index 747aabcee..b4582238e 100644 --- a/packages/components/modules/files/web/FileUploadList/index.tsx +++ b/packages/components/modules/files/web/FileUploadList/index.tsx @@ -3,6 +3,7 @@ import type { FC, ReactNode } from 'react' import { Box, Stack, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { useScopedUploads } from '../../common/context/useScopedUploads' @@ -21,6 +22,7 @@ const FileUploadList: FC = ({ editable = false, scope, }) => { + const intl = useIntl() const target = useFragment(FilesListFragment, targetRef) // The upload store is global; show only uploads owned by this list (its @@ -94,7 +96,10 @@ const FileUploadList: FC = ({ {showUploadProgress && hasUploadingFiles && ( - Uploading ({uploadingFiles.length}) + {intl.formatMessage( + { id: 'files.uploadList.uploading', defaultMessage: 'Uploading ({count})' }, + { count: uploadingFiles.length }, + )} {uploadingFiles.map((file) => ( = ({ {hasAttachedFiles && target.files && ( - Attached Files ({target.files.edges.length}) + {intl.formatMessage( + { id: 'files.uploadList.attached', defaultMessage: 'Attached Files ({count})' }, + { count: target.files.edges.length }, + )} {target.files.edges.map((edge) => { if (!edge?.node) return null diff --git a/packages/components/modules/files/web/FileUploadTrigger/index.tsx b/packages/components/modules/files/web/FileUploadTrigger/index.tsx index ca606bbc5..9e6cfdb9d 100644 --- a/packages/components/modules/files/web/FileUploadTrigger/index.tsx +++ b/packages/components/modules/files/web/FileUploadTrigger/index.tsx @@ -6,6 +6,7 @@ import { IconButton } from '@baseapp-frontend/design-system/components/web/butto import { AttachFile as AttachFileIcon } from '@mui/icons-material' import { Tooltip } from '@mui/material' +import { useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { FilesListFragment } from '../../common/graphql/queries/FilesList' @@ -23,7 +24,7 @@ const FileUploadTrigger: FC = ({ target: targetRef, as = 'dropzone', icon, - label = 'Attach files', + label: labelProp, maxFiles, maxFileSize, acceptedFileTypes, @@ -33,7 +34,11 @@ const FileUploadTrigger: FC = ({ onAttachComplete, onError, }) => { + const intl = useIntl() const target = useFragment(FilesListFragment, targetRef) + const label = + labelProp ?? + intl.formatMessage({ id: 'files.uploadTrigger.label', defaultMessage: 'Attach files' }) const { handleFilesSelected, isAttaching, resetKey } = useFileUploadLogic({ targetObjectId: target.id, diff --git a/packages/components/modules/messages/common/useLeaveGroup/index.tsx b/packages/components/modules/messages/common/useLeaveGroup/index.tsx index 31485a7d2..d65eb3021 100644 --- a/packages/components/modules/messages/common/useLeaveGroup/index.tsx +++ b/packages/components/modules/messages/common/useLeaveGroup/index.tsx @@ -17,26 +17,33 @@ const UseLeaveGroup = ({ roomId, isSoleAdmin = false, onClose, + removeSuccessMessage = 'Member was successfully removed', }: UseLeaveGroupProps) => { const [commit, isMutationInFlight] = useUpdateChatRoomMutation() const { sendMutationErrorToast, sendToast } = useNotification() + const getLeaveGroupDialogTextCopyKeys = () => { + if (profileId === removingParticipantId) { + return { + action: LEAVE_GROUP_DIALOG_TEXT_COPY_ACTION_KEYS.IS_LEAVING, + role: isSoleAdmin + ? LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS.ADMIN + : LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS.MEMBER, + } as const + } + return { + action: LEAVE_GROUP_DIALOG_TEXT_COPY_ACTION_KEYS.IS_REMOVING, + role: LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS.ADMIN, + } as const + } + const getLeaveGroupDialogTextCopy = ( type: ValueOf, ) => { - if (profileId === removingParticipantId) { - if (isSoleAdmin) { - return LEAVE_GROUP_DIALOG_TEXT_COPY[LEAVE_GROUP_DIALOG_TEXT_COPY_ACTION_KEYS.IS_LEAVING][ - LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS.ADMIN - ][type] - } - return LEAVE_GROUP_DIALOG_TEXT_COPY[LEAVE_GROUP_DIALOG_TEXT_COPY_ACTION_KEYS.IS_LEAVING][ - LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS.MEMBER - ][type] - } - return LEAVE_GROUP_DIALOG_TEXT_COPY[LEAVE_GROUP_DIALOG_TEXT_COPY_ACTION_KEYS.IS_REMOVING][ - LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS.ADMIN - ][type] + const { action, role } = getLeaveGroupDialogTextCopyKeys() + return action === LEAVE_GROUP_DIALOG_TEXT_COPY_ACTION_KEYS.IS_LEAVING + ? LEAVE_GROUP_DIALOG_TEXT_COPY[action][role][type] + : LEAVE_GROUP_DIALOG_TEXT_COPY[action][LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS.ADMIN][type] } const onRemoveConfirmed = () => { @@ -55,7 +62,7 @@ const UseLeaveGroup = ({ // flow isn't form-backed, so surface the payload errors here. const errorMessage = sendMutationErrorToast(response?.chatRoomUpdate?.errors, undefined) if (!errorMessage && removingParticipantId && removingParticipantId !== profileId) { - sendToast('Member was successfully removed') + sendToast(removeSuccessMessage) } onClose() }, @@ -67,6 +74,7 @@ const UseLeaveGroup = ({ return { getLeaveGroupDialogTextCopy, + getLeaveGroupDialogTextCopyKeys, onRemoveConfirmed, isMutationInFlight, } diff --git a/packages/components/modules/messages/common/useLeaveGroup/types.ts b/packages/components/modules/messages/common/useLeaveGroup/types.ts index ab5f48885..0e69f3072 100644 --- a/packages/components/modules/messages/common/useLeaveGroup/types.ts +++ b/packages/components/modules/messages/common/useLeaveGroup/types.ts @@ -4,4 +4,5 @@ export interface UseLeaveGroupProps { profileId: string removingParticipantId?: string | null roomId?: string + removeSuccessMessage?: string } diff --git a/packages/components/modules/messages/common/utils.ts b/packages/components/modules/messages/common/utils.ts index ad628c506..40fb18169 100644 --- a/packages/components/modules/messages/common/utils.ts +++ b/packages/components/modules/messages/common/utils.ts @@ -35,6 +35,7 @@ export const useSingleChatDetails = (chatRef: SingleChatDetailsFragment$key) => return { roomId: id, isGroup, + isDeletedUser: true, title: 'Deleted User', image: undefined, } @@ -43,6 +44,7 @@ export const useSingleChatDetails = (chatRef: SingleChatDetailsFragment$key) => return { roomId: id, isGroup, + isDeletedUser: true, title: 'Deleted User', image: undefined, username: undefined, diff --git a/packages/components/modules/messages/web/AddContactToGroupDialog/GroupsList/GroupItem/index.tsx b/packages/components/modules/messages/web/AddContactToGroupDialog/GroupsList/GroupItem/index.tsx index 7acae1840..c62d0632f 100644 --- a/packages/components/modules/messages/web/AddContactToGroupDialog/GroupsList/GroupItem/index.tsx +++ b/packages/components/modules/messages/web/AddContactToGroupDialog/GroupsList/GroupItem/index.tsx @@ -5,13 +5,16 @@ import { FC } from 'react' import { AvatarWithPlaceholder } from '@baseapp-frontend/design-system/components/web/avatars' import { Box, Checkbox, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useFragment } from 'react-relay' -import { AddContactToGroupItemFragment, getParticipantCountString } from '../../../../common' +import { AddContactToGroupItemFragment } from '../../../../common' +import { getParticipantCountLabel } from '../../../__shared__/utils' import { MainContainer } from './styled' import { GroupItemProps } from './types' const GroupItem: FC = ({ roomRef, selected, onToggle }) => { + const intl = useIntl() const node = useFragment(AddContactToGroupItemFragment, roomRef) const isAlreadyMember = !!node.isParticipant @@ -34,10 +37,13 @@ const GroupItem: FC = ({ roomRef, selected, onToggle }) => { {node.title} - {getParticipantCountString(node.participantsCount)} + {getParticipantCountLabel(intl, node.participantsCount)} {isAlreadyMember && ( - {' • Already added'} + {intl.formatMessage({ + id: 'messages.addContactToGroup.groupItem.alreadyAdded', + defaultMessage: ' • Already added', + })} )} @@ -47,7 +53,15 @@ const GroupItem: FC = ({ roomRef, selected, onToggle }) => { disabled={isAlreadyMember} onChange={handleToggle} onClick={(event) => event.stopPropagation()} - inputProps={{ 'aria-label': `select group ${node.title}` }} + inputProps={{ + 'aria-label': intl.formatMessage( + { + id: 'messages.addContactToGroup.groupItem.selectAriaLabel', + defaultMessage: 'select group {title}', + }, + { title: node.title }, + ), + }} /> ) diff --git a/packages/components/modules/messages/web/AddContactToGroupDialog/GroupsList/index.tsx b/packages/components/modules/messages/web/AddContactToGroupDialog/GroupsList/index.tsx index 7cad5c7dd..ef3da9091 100644 --- a/packages/components/modules/messages/web/AddContactToGroupDialog/GroupsList/index.tsx +++ b/packages/components/modules/messages/web/AddContactToGroupDialog/GroupsList/index.tsx @@ -8,6 +8,7 @@ import { Searchbar } from '@baseapp-frontend/design-system/components/web/inputs import { Box, Typography } from '@mui/material' import { useForm } from 'react-hook-form' +import { FormattedMessage, useIntl } from 'react-intl' import { useLazyLoadQuery } from 'react-relay' import { Virtuoso } from 'react-virtuoso' @@ -21,6 +22,7 @@ import { SearchbarContainer } from './styled' import { GroupsListProps } from './types' const GroupsList: FC = ({ contactProfileId, selectedIds, onToggle }) => { + const intl = useIntl() const { currentProfile } = useCurrentProfile() const profileId = currentProfile?.id ?? '' const [isPending, startTransition] = useTransition() @@ -74,7 +76,10 @@ const GroupsList: FC = ({ contactProfileId, selectedIds, onTogg ) } @@ -87,10 +92,16 @@ const GroupsList: FC = ({ contactProfileId, selectedIds, onTogg return ( - No groups yet + - You can only add contacts to groups you manage. + ) @@ -127,18 +138,25 @@ const GroupsList: FC = ({ contactProfileId, selectedIds, onTogg ) } -const SuspendedGroupsList: FC = (props) => ( - - } - > - - -) +const SuspendedGroupsList: FC = (props) => { + const intl = useIntl() + + return ( + + } + > + + + ) +} export default SuspendedGroupsList diff --git a/packages/components/modules/messages/web/AddContactToGroupDialog/index.tsx b/packages/components/modules/messages/web/AddContactToGroupDialog/index.tsx index bb8a3b3d5..85053ae7b 100644 --- a/packages/components/modules/messages/web/AddContactToGroupDialog/index.tsx +++ b/packages/components/modules/messages/web/AddContactToGroupDialog/index.tsx @@ -7,8 +7,10 @@ import { ConfirmDialog } from '@baseapp-frontend/design-system/components/web/di import { useNotification } from '@baseapp-frontend/utils' import { LoadingButton } from '@mui/lab' +import { FormattedMessage, useIntl } from 'react-intl' import { toggleGroupSelection, useAddParticipantToChatRoomsMutation } from '../../common' +import { SHARED_MESSAGES } from '../__shared__/constants' import GroupsList from './GroupsList' import { AddContactToGroupDialogProps } from './types' @@ -17,6 +19,7 @@ const AddContactToGroupDialog: FC = ({ open, onClose, }) => { + const intl = useIntl() const { sendToast } = useNotification() const { currentProfile } = useCurrentProfile() const profileId = currentProfile?.id ?? '' @@ -50,7 +53,7 @@ const AddContactToGroupDialog: FC = ({ onCompleted: (response) => { const errors = response?.chatRoomsAddParticipant?.errors if (errors?.length) { - sendToast('Something went wrong', { type: 'error' }) + sendToast(intl.formatMessage(SHARED_MESSAGES.somethingWentWrong), { type: 'error' }) } else { handleClose() } @@ -60,7 +63,10 @@ const AddContactToGroupDialog: FC = ({ return ( = ({ disabled={confirmDisabled} loading={isMutationInFlight} > - Add to Group + } onClose={handleClose} diff --git a/packages/components/modules/messages/web/AllChatRoomsList/ChatRoomItem/constants.ts b/packages/components/modules/messages/web/AllChatRoomsList/ChatRoomItem/constants.ts new file mode 100644 index 000000000..4ab201cb1 --- /dev/null +++ b/packages/components/modules/messages/web/AllChatRoomsList/ChatRoomItem/constants.ts @@ -0,0 +1,28 @@ +import { defineMessages } from 'react-intl' + +export const CHAT_ROOM_ITEM_MESSAGES = defineMessages({ + itemTitle: { + id: 'messages.chatRoomItem.itemTitle', + defaultMessage: 'Chat', + }, + markAsUnread: { + id: 'messages.chatRoomItem.markAsUnread', + defaultMessage: 'Mark as Unread', + }, + deleteDialogTitle: { + id: 'messages.chatRoomItem.deleteDialog.title', + defaultMessage: 'Delete Chat?', + }, + monthsAgo: { + id: 'messages.chatRoomItem.date.monthsAgo', + defaultMessage: '{count, plural, one {# month ago} other {# months ago}}', + }, + weeksAgo: { + id: 'messages.chatRoomItem.date.weeksAgo', + defaultMessage: '{count, plural, one {# week ago} other {# weeks ago}}', + }, + daysAgo: { + id: 'messages.chatRoomItem.date.daysAgo', + defaultMessage: '{count, plural, one {# day ago} other {# days ago}}', + }, +}) diff --git a/packages/components/modules/messages/web/AllChatRoomsList/ChatRoomItem/index.tsx b/packages/components/modules/messages/web/AllChatRoomsList/ChatRoomItem/index.tsx index 6a339032b..ab104b769 100644 --- a/packages/components/modules/messages/web/AllChatRoomsList/ChatRoomItem/index.tsx +++ b/packages/components/modules/messages/web/AllChatRoomsList/ChatRoomItem/index.tsx @@ -10,6 +10,7 @@ import { import { TypographyWithEllipsis } from '@baseapp-frontend/design-system/components/web/typographies' import { Box, Badge as DefaultBadge, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { LastMessageFragment$key } from '../../../../../__generated__/LastMessageFragment.graphql' @@ -24,6 +25,8 @@ import { useTitleAndImage, useUnreadChatMutation, } from '../../../common' +import { SHARED_MESSAGES } from '../../__shared__/constants' +import { CHAT_ROOM_ITEM_MESSAGES } from './constants' import { Dot, StyledChatCard } from './styled' import { ChatRoomItemProps } from './types' import { formatDate, getLastMessagePreview } from './utils' @@ -36,6 +39,7 @@ const ChatRoomItem: FC = ({ BadgeProps = {}, isInArchivedTab = false, }) => { + const intl = useIntl() const lastMessageFragment = useFragment(LastMessageFragment, roomRef) const headerFragment = useFragment(TitleFragment, roomRef) const unreadMessagesCountFragment = useFragment( @@ -76,13 +80,15 @@ const ChatRoomItem: FC = ({ return ( : , - label: !isInArchivedTab ? 'Archive Chat' : 'Unarchive Chat', + label: intl.formatMessage( + !isInArchivedTab ? SHARED_MESSAGES.archiveChat : SHARED_MESSAGES.unarchiveChat, + ), onClick: () => { if (currentProfile?.id) { commit({ @@ -101,7 +107,7 @@ const ChatRoomItem: FC = ({ { disabled: hasUnreadMessages, icon: , - label: 'Mark as Unread', + label: intl.formatMessage(CHAT_ROOM_ITEM_MESSAGES.markAsUnread), onClick: unreadChat, hasPermission: true, closeOnClick: true, @@ -110,6 +116,7 @@ const ChatRoomItem: FC = ({ showDeleteButton handleDeleteItem={() => {}} isDeletingItem={false} + DeleteDialogProps={{ title: intl.formatMessage(CHAT_ROOM_ITEM_MESSAGES.deleteDialogTitle) }} ref={chatCardRef} > = ({ alignItems="center" > - {formatDate(lastMessageTime)} + {formatDate(intl, lastMessageTime)} diff --git a/packages/components/modules/messages/web/AllChatRoomsList/ChatRoomItem/utils.ts b/packages/components/modules/messages/web/AllChatRoomsList/ChatRoomItem/utils.ts index ef7326d53..c5c0e943e 100644 --- a/packages/components/modules/messages/web/AllChatRoomsList/ChatRoomItem/utils.ts +++ b/packages/components/modules/messages/web/AllChatRoomsList/ChatRoomItem/utils.ts @@ -7,6 +7,10 @@ import { } from '@baseapp-frontend/utils' import { DateTime } from 'luxon' +import { IntlShape } from 'react-intl' + +import { SHARED_MESSAGES } from '../../__shared__/constants' +import { CHAT_ROOM_ITEM_MESSAGES } from './constants' // Bound every regex pass so adversarial input cannot cause super-linear runtime: // the input length is capped and every lazy-quantifier match is constrained to a @@ -50,28 +54,31 @@ export const getLastMessagePreview = (content?: string | null) => { ) } -export const formatDate = (date?: string | null) => { +export const formatDate = (intl: IntlShape, date?: string | null) => { if (!date) return '' const dateTime = DateTime.fromISO(date) if (!dateTime.isValid) return '' if (isToday(date)) return formatDateFromApi(date, { toFormat: TIME_FORMAT[2] }) - if (isYesterday(date)) return 'Yesterday' + if (isYesterday(date)) return intl.formatMessage(SHARED_MESSAGES.yesterday) const diff = dateTime.diffNow(['years', 'months', 'weeks', 'days']).toObject() if (diff.months && Math.abs(diff.months) > 0) { - if (Math.abs(diff.months) > 1) return `${Math.abs(diff.months).toFixed(0)} months ago` - return '1 month ago' + const count = Math.abs(diff.months) > 1 ? Math.round(Math.abs(diff.months)) : 1 + return intl.formatMessage(CHAT_ROOM_ITEM_MESSAGES.monthsAgo, { count }) } if (diff.weeks && Math.abs(diff.weeks) > 0) { - if (Math.abs(diff.weeks) > 1) return `${Math.abs(diff.weeks).toFixed(0)} weeks ago` - return '1 week ago' + const count = Math.abs(diff.weeks) > 1 ? Math.round(Math.abs(diff.weeks)) : 1 + return intl.formatMessage(CHAT_ROOM_ITEM_MESSAGES.weeksAgo, { count }) } - if (diff.days && Math.abs(diff.days) > 0) return `${Math.abs(diff.days).toFixed(0)} days ago` + if (diff.days && Math.abs(diff.days) > 0) + return intl.formatMessage(CHAT_ROOM_ITEM_MESSAGES.daysAgo, { + count: Math.round(Math.abs(diff.days)), + }) return formatDateFromApi(date, { toFormat: DATE_FORMAT[2] }) } diff --git a/packages/components/modules/messages/web/AllChatRoomsList/EmptyChatRoomsState/index.tsx b/packages/components/modules/messages/web/AllChatRoomsList/EmptyChatRoomsState/index.tsx index 7ce5963ae..a11cb1ef9 100644 --- a/packages/components/modules/messages/web/AllChatRoomsList/EmptyChatRoomsState/index.tsx +++ b/packages/components/modules/messages/web/AllChatRoomsList/EmptyChatRoomsState/index.tsx @@ -1,12 +1,16 @@ import { SocialMediaDiscussionImage } from '@baseapp-frontend/design-system/components/web/illustrations' import { Box, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' const EmptyChatRoomsState = () => ( - No messages to be displayed. + ) diff --git a/packages/components/modules/messages/web/AllChatRoomsList/constants.ts b/packages/components/modules/messages/web/AllChatRoomsList/constants.ts index 3cc59cd84..b207778d0 100644 --- a/packages/components/modules/messages/web/AllChatRoomsList/constants.ts +++ b/packages/components/modules/messages/web/AllChatRoomsList/constants.ts @@ -1,3 +1,5 @@ +import { defineMessages } from 'react-intl' + export const CHAT_TAB_VALUES = { active: 'active', unread: 'unread', @@ -5,9 +7,12 @@ export const CHAT_TAB_VALUES = { groups: 'groups', } as const -export const CHAT_TAB_LABEL = { - [CHAT_TAB_VALUES.active]: 'Active', - [CHAT_TAB_VALUES.unread]: 'Unread', - [CHAT_TAB_VALUES.archived]: 'Archived', - [CHAT_TAB_VALUES.groups]: 'Groups', -} as const +export const CHAT_TAB_LABEL = defineMessages({ + [CHAT_TAB_VALUES.active]: { id: 'messages.chatRoomsList.tabs.active', defaultMessage: 'Active' }, + [CHAT_TAB_VALUES.unread]: { id: 'messages.chatRoomsList.tabs.unread', defaultMessage: 'Unread' }, + [CHAT_TAB_VALUES.archived]: { + id: 'messages.chatRoomsList.tabs.archived', + defaultMessage: 'Archived', + }, + [CHAT_TAB_VALUES.groups]: { id: 'messages.chatRoomsList.tabs.groups', defaultMessage: 'Groups' }, +}) diff --git a/packages/components/modules/messages/web/AllChatRoomsList/index.tsx b/packages/components/modules/messages/web/AllChatRoomsList/index.tsx index 8392665a2..5fc011d56 100644 --- a/packages/components/modules/messages/web/AllChatRoomsList/index.tsx +++ b/packages/components/modules/messages/web/AllChatRoomsList/index.tsx @@ -16,6 +16,7 @@ import { Searchbar as DefaultSearchbar } from '@baseapp-frontend/design-system/c import { Box, Button, Tab, Tabs, Typography } from '@mui/material' import { useForm } from 'react-hook-form' +import { FormattedMessage, useIntl } from 'react-intl' import { Virtuoso } from 'react-virtuoso' import { RoomsListFragment$key } from '../../../../__generated__/RoomsListFragment.graphql' @@ -37,6 +38,7 @@ const AllChatRoomsList: FC = ({ EmptyChatRoomsState = DefaultEmptyChatRoomsState, VirtuosoProps = {}, }) => { + const intl = useIntl() const [tab, setTab] = useState(CHAT_TAB_VALUES.active) const [renderList, setRenderList] = useState(true) @@ -140,7 +142,10 @@ const AllChatRoomsList: FC = ({ ) } @@ -151,7 +156,7 @@ const AllChatRoomsList: FC = ({ return ( - {CHAT_TAB_LABEL[tabValue]} + {intl.formatMessage(CHAT_TAB_LABEL[tabValue])} {isLoadingTab && @@ -174,7 +179,7 @@ const AllChatRoomsList: FC = ({
- Messages +
diff --git a/packages/components/modules/messages/web/ChatCreate/Body/index.tsx b/packages/components/modules/messages/web/ChatCreate/Body/index.tsx index ee1d53c33..a2bfea888 100644 --- a/packages/components/modules/messages/web/ChatCreate/Body/index.tsx +++ b/packages/components/modules/messages/web/ChatCreate/Body/index.tsx @@ -6,6 +6,8 @@ import { AvatarButton } from '@baseapp-frontend/design-system/components/web/but import { NewGroupIcon } from '@baseapp-frontend/design-system/components/web/icons' import { Searchbar as DefaultSearchbar } from '@baseapp-frontend/design-system/components/web/inputs' +import { useIntl } from 'react-intl' + import { MainContainer, SearchbarContainer } from './styled' import { BodyProps } from './types' @@ -13,32 +15,42 @@ const Body: FC = ({ AvatarButtonIcon = NewGroupIcon, children, control, - groupChatCreateButtonCaption = 'New Group', + groupChatCreateButtonCaption, handleSearchChange, handleSearchClear, isPending, onGroupChatCreationButtonClicked, Searchbar = DefaultSearchbar, SearchbarProps = {}, -}) => ( - - - { + const intl = useIntl() + + return ( + + + + + - - - {children} - -) + {children} + + ) +} export default Body diff --git a/packages/components/modules/messages/web/ChatCreate/ChatRoomListItem/index.tsx b/packages/components/modules/messages/web/ChatCreate/ChatRoomListItem/index.tsx index bc2bded25..e078c7d12 100644 --- a/packages/components/modules/messages/web/ChatCreate/ChatRoomListItem/index.tsx +++ b/packages/components/modules/messages/web/ChatCreate/ChatRoomListItem/index.tsx @@ -8,6 +8,7 @@ import { TypographyWithEllipsis } from '@baseapp-frontend/design-system/componen import { LoadingButton } from '@mui/lab' import { Box, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' import { ConnectionHandler, useFragment } from 'react-relay' import { formatHandle } from '../../../../__shared__/common/utils' @@ -65,7 +66,7 @@ const ChatRoomListItem: FC = ({ profile: profileRef, onCh loading={isMutationInFlight} sx={{ maxWidth: 'fit-content', justifySelf: 'end' }} > - Message + ) diff --git a/packages/components/modules/messages/web/ChatCreate/Header/index.tsx b/packages/components/modules/messages/web/ChatCreate/Header/index.tsx index 9ce28fca9..bfc88be81 100644 --- a/packages/components/modules/messages/web/ChatCreate/Header/index.tsx +++ b/packages/components/modules/messages/web/ChatCreate/Header/index.tsx @@ -6,7 +6,9 @@ import { IconButton } from '@baseapp-frontend/design-system/components/web/butto import { Iconify } from '@baseapp-frontend/design-system/components/web/images' import { Box, Typography } from '@mui/material' +import { useIntl } from 'react-intl' +import { SHARED_MESSAGES } from '../../__shared__/constants' import { HeaderContainer } from './styled' import { HeaderProps } from './types' @@ -14,30 +16,43 @@ const Header: FC = ({ backIcon = 'eva:arrow-ios-back-fill', backIconProps = {}, onHeaderClick, - title = 'New Chat', + title, titleProps = {}, -}) => ( - - - { + const intl = useIntl() + + return ( + + - - - - {title} - -
- - -) + + + + + {title ?? + intl.formatMessage({ + id: 'messages.chatCreate.header.title', + defaultMessage: 'New Chat', + })} + +
+ + + ) +} export default Header diff --git a/packages/components/modules/messages/web/ChatCreate/index.tsx b/packages/components/modules/messages/web/ChatCreate/index.tsx index 24e15b489..909b21260 100644 --- a/packages/components/modules/messages/web/ChatCreate/index.tsx +++ b/packages/components/modules/messages/web/ChatCreate/index.tsx @@ -8,11 +8,13 @@ import { Searchbar as DefaultSearchbar } from '@baseapp-frontend/design-system/c import { Box } from '@mui/material' import { useForm } from 'react-hook-form' +import { useIntl } from 'react-intl' import { Virtuoso } from 'react-virtuoso' import { SearchNotFoundState } from '../../../__shared__/web' import { ProfileEdge, ProfileNode, useAllProfilesList } from '../../../profiles/common' import EmptyProfilesListState from '../__shared__/EmptyProfilesListState' +import { SHARED_MESSAGES } from '../__shared__/constants' import DefaultBody from './Body' import DefaultChatRoomListItem from './ChatRoomListItem' import DefaultHeader from './Header' @@ -33,6 +35,7 @@ const ChatCreate: FC = ({ SearchbarProps = {}, VirtuosoProps = {}, }) => { + const intl = useIntl() const { data: { allProfiles }, loadNext, @@ -74,7 +77,7 @@ const ChatCreate: FC = ({ ) } diff --git a/packages/components/modules/messages/web/ChatRoom/ChatRoomHeader/ChatRoomOptions/index.tsx b/packages/components/modules/messages/web/ChatRoom/ChatRoomHeader/ChatRoomOptions/index.tsx index 6e7b34cfc..f4611bfcf 100644 --- a/packages/components/modules/messages/web/ChatRoom/ChatRoomHeader/ChatRoomOptions/index.tsx +++ b/packages/components/modules/messages/web/ChatRoom/ChatRoomHeader/ChatRoomOptions/index.tsx @@ -1,7 +1,9 @@ import { FC } from 'react' import { MenuItem, MenuList, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' +import { SHARED_MESSAGES } from '../../../__shared__/constants' import { ChatRoomOptionsProps } from './types' const ChatRoomOptions: FC = ({ @@ -15,22 +17,33 @@ const ChatRoomOptions: FC = ({ }) => ( - {isArchived ? 'Unarchive Chat' : 'Archive Chat'} + + + {isGroup ? ( <> - Group Details + + + - Leave Group + ) : ( - Contact Details + + + )} diff --git a/packages/components/modules/messages/web/ChatRoom/ChatRoomHeader/index.tsx b/packages/components/modules/messages/web/ChatRoom/ChatRoomHeader/index.tsx index 9f3f143bd..1995845e3 100644 --- a/packages/components/modules/messages/web/ChatRoom/ChatRoomHeader/index.tsx +++ b/packages/components/modules/messages/web/ChatRoom/ChatRoomHeader/index.tsx @@ -11,14 +11,12 @@ import { usePopover } from '@baseapp-frontend/design-system/hooks/common' import { useResponsive } from '@baseapp-frontend/design-system/hooks/web' import { Box, Typography } from '@mui/material' +import { useIntl } from 'react-intl' -import { - getParticipantCountString, - useArchiveChatRoomMutation, - useChatRoom, - useSingleChatDetails, -} from '../../../common' +import { useArchiveChatRoomMutation, useChatRoom, useSingleChatDetails } from '../../../common' import LeaveGroupDialog from '../../__shared__/LeaveGroupDialog' +import { SHARED_MESSAGES } from '../../__shared__/constants' +import { getParticipantCountLabel } from '../../__shared__/utils' import ChatRoomOptions from './ChatRoomOptions' import { BackButtonContainer, ChatHeaderContainer, ChatTitleContainer } from './styled' import { ChatRoomHeaderProps } from './types' @@ -31,14 +29,15 @@ const ChatRoomHeader: FC = ({ onDisplayProfileSummaryClicked, roomId, }) => { + const intl = useIntl() const [open, setOpen] = useState(false) const { currentProfile } = useCurrentProfile() const isUpToMd = useResponsive('up', 'md') const { resetChatRoom } = useChatRoom() - const { isGroup, isSoleAdmin, title, image } = useSingleChatDetails(roomTitleRef) - const members = getParticipantCountString(participantsCount) + const { isGroup, isSoleAdmin, title, image, isDeletedUser } = useSingleChatDetails(roomTitleRef) + const members = getParticipantCountLabel(intl, participantsCount) const popover = usePopover() const [commit, isMutationInFlight] = useArchiveChatRoomMutation() @@ -77,7 +76,10 @@ const ChatRoomHeader: FC = ({ ) : ( @@ -105,7 +107,7 @@ const ChatRoomHeader: FC = ({ clear: 'left', }} > - {title} + {isDeletedUser ? intl.formatMessage(SHARED_MESSAGES.deletedUser) : title} {isGroup && ( @@ -114,7 +116,13 @@ const ChatRoomHeader: FC = ({ )} - + = ({ // TODO: handle error for chatRoom if (!chatRoom) { - return
Chat room not found
+ return ( +
+ +
+ ) } return ( diff --git a/packages/components/modules/messages/web/GroupChatCreate/Header/index.tsx b/packages/components/modules/messages/web/GroupChatCreate/Header/index.tsx index 845e15d5d..7fa27ff27 100644 --- a/packages/components/modules/messages/web/GroupChatCreate/Header/index.tsx +++ b/packages/components/modules/messages/web/GroupChatCreate/Header/index.tsx @@ -9,6 +9,7 @@ import { } from '@baseapp-frontend/design-system/components/web/icons' import { Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { HeaderContainer } from './styled' import { HeaderProps } from './types' @@ -19,20 +20,41 @@ const Header: FC = ({ isDisabled, onBackButtonClicked, onCreateButtonClicked, - title = 'New Group', + title, titleProps = {}, -}) => ( - - - - - - {title} - - - - - -) +}) => { + const intl = useIntl() + + return ( + + + + + + {title ?? + intl.formatMessage({ + id: 'messages.groupChatCreate.header.title', + defaultMessage: 'New Group', + })} + + + + + + ) +} export default Header diff --git a/packages/components/modules/messages/web/GroupChatCreate/index.tsx b/packages/components/modules/messages/web/GroupChatCreate/index.tsx index f3654453e..3f13a505c 100644 --- a/packages/components/modules/messages/web/GroupChatCreate/index.tsx +++ b/packages/components/modules/messages/web/GroupChatCreate/index.tsx @@ -8,18 +8,19 @@ import { filterDirtyValues, setFormRelayErrors } from '@baseapp-frontend/utils' import { zodResolver } from '@hookform/resolvers/zod' import { Box } from '@mui/material' import { useForm } from 'react-hook-form' +import { useIntl } from 'react-intl' import { ConnectionHandler } from 'relay-runtime' import { ProfileNode, useAllProfilesList } from '../../../profiles/common' import { useChatRoom, useCreateChatRoomMutation } from '../../common' import { - DEFAULT_CREATE_OR_EDIT_GROUP_FORM_VALIDATION as DEFAULT_FORM_VALIDATION, DEFAULT_CREATE_OR_EDIT_GROUP_FORM_VALUE as DEFAULT_FORM_VALUES, CREATE_OR_EDIT_GROUP_FORM_VALUE as FORM_VALUE, } from '../../common/constants' import { CreateOrEditGroup } from '../../common/types' import DefaultEditGroupTitleAndImage from '../__shared__/EditGroupTitleAndImage' import DefaultGroupChatMembersList from '../__shared__/GroupChatMembersList' +import { getCreateOrEditGroupFormValidation } from '../__shared__/constants' import DefaultHeader from './Header' import { ProfilesContainer } from './styled' import { GroupChatCreateProps } from './types' @@ -43,10 +44,12 @@ const GroupChatCreate: FC = ({ refetch, } = useAllProfilesList(allProfilesRef) + const intl = useIntl() + const formValidation = useMemo(() => getCreateOrEditGroupFormValidation(intl), [intl]) const formReturn = useForm({ defaultValues: DEFAULT_FORM_VALUES, // @ts-ignore TODO: check typing issue with zodResolver - resolver: zodResolver(DEFAULT_FORM_VALIDATION), + resolver: zodResolver(formValidation), mode: 'onBlur', }) diff --git a/packages/components/modules/messages/web/GroupChatDetails/Body/index.tsx b/packages/components/modules/messages/web/GroupChatDetails/Body/index.tsx index 012dca198..e2c2cd549 100644 --- a/packages/components/modules/messages/web/GroupChatDetails/Body/index.tsx +++ b/packages/components/modules/messages/web/GroupChatDetails/Body/index.tsx @@ -6,8 +6,10 @@ import { CircledAvatar } from '@baseapp-frontend/design-system/components/web/av import { TypographyWithEllipsis } from '@baseapp-frontend/design-system/components/web/typographies' import { Box, Typography, useTheme } from '@mui/material' +import { useIntl } from 'react-intl' -import { getParticipantCountString } from '../../../common' +import { SHARED_MESSAGES } from '../../__shared__/constants' +import { getParticipantCountLabel } from '../../__shared__/utils' import { GroupHeaderContainer, GroupTitleContainer, MembersContainer } from './styled' import { BodyProps } from './types' @@ -21,6 +23,7 @@ const Body: FC = ({ titleProps = {}, }) => { const theme = useTheme() + const intl = useIntl() return ( @@ -36,12 +39,18 @@ const Body: FC = ({ {title} - {getParticipantCountString(participantsCount)} + {getParticipantCountLabel(intl, participantsCount)} - + = ({ borderBottom: `1px solid ${theme.palette.divider}`, }} > - Members + {intl.formatMessage(SHARED_MESSAGES.members)} {children} diff --git a/packages/components/modules/messages/web/GroupChatDetails/Header/index.tsx b/packages/components/modules/messages/web/GroupChatDetails/Header/index.tsx index efde3b97e..f7e8c382b 100644 --- a/packages/components/modules/messages/web/GroupChatDetails/Header/index.tsx +++ b/packages/components/modules/messages/web/GroupChatDetails/Header/index.tsx @@ -5,7 +5,9 @@ import { PenEditIcon } from '@baseapp-frontend/design-system/components/web/icon import { Iconify } from '@baseapp-frontend/design-system/components/web/images' import { Typography } from '@mui/material' +import { useIntl } from 'react-intl' +import { SHARED_MESSAGES } from '../../__shared__/constants' import { GroupDetailsHeaderContainer } from './styled' import { HeaderProps } from './types' @@ -16,28 +18,38 @@ const Header: FC = ({ onBackButtonClicked, onEditButtonClicked, shouldDisplayEditButton, -}) => ( - - - - - - Group View - - {shouldDisplayEditButton && ( +}) => { + const intl = useIntl() + + return ( + - + - )} - -) + + {intl.formatMessage({ + id: 'messages.groupChatDetails.header.title', + defaultMessage: 'Group View', + })} + + {shouldDisplayEditButton && ( + + + + )} + + ) +} export default Header diff --git a/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/AdminOptionsMenu/index.tsx b/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/AdminOptionsMenu/index.tsx index d30f65412..7777478d9 100644 --- a/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/AdminOptionsMenu/index.tsx +++ b/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/AdminOptionsMenu/index.tsx @@ -1,7 +1,9 @@ import { FC } from 'react' import { MenuItem, MenuList, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' +import { SHARED_MESSAGES } from '../../../__shared__/constants' import { AdminOptionsProps } from './types' const AdminOptionsMenu: FC = ({ @@ -12,16 +14,31 @@ const AdminOptionsMenu: FC = ({ }) => ( - See Profile + + + - {isAdmin ? 'Remove admin permissions' : 'Promote to admin'} + {isAdmin ? ( + + ) : ( + + )} - Remove from group + diff --git a/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/MemberOptionsMenu/index.tsx b/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/MemberOptionsMenu/index.tsx index 18a13bcbe..5e914f6fe 100644 --- a/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/MemberOptionsMenu/index.tsx +++ b/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/MemberOptionsMenu/index.tsx @@ -1,7 +1,9 @@ import { FC } from 'react' import { MenuItem, MenuList, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' +import { SHARED_MESSAGES } from '../../../__shared__/constants' import { MemberOptionsMenuProps } from './types' const MemberOptionsMenu: FC = ({ @@ -11,12 +13,14 @@ const MemberOptionsMenu: FC = ({ }) => ( - See Profile + + + {isMe && ( - Leave Group + )} diff --git a/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/RemoveAdminPermissionsDialog/index.tsx b/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/RemoveAdminPermissionsDialog/index.tsx index 0ed0e191a..ac71b4efe 100644 --- a/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/RemoveAdminPermissionsDialog/index.tsx +++ b/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/RemoveAdminPermissionsDialog/index.tsx @@ -3,6 +3,7 @@ import { FC } from 'react' import { ConfirmDialog } from '@baseapp-frontend/design-system/components/web/dialogs' import { LoadingButton } from '@mui/lab' +import { useIntl } from 'react-intl' import { RemoveAdminPermissionsDialogProps } from './types' @@ -11,23 +12,37 @@ const RemoveAdminPermissionsDialog: FC = ({ onClose, isMutationInFlight, onRemoveConfirmed, -}) => ( - - Remove permissions - - } - onClose={onClose} - open={open} - /> -) +}) => { + const intl = useIntl() + + return ( + + {intl.formatMessage({ + id: 'messages.groupChatDetails.removeAdminDialog.confirm', + defaultMessage: 'Remove permissions', + })} + + } + onClose={onClose} + open={open} + /> + ) +} export default RemoveAdminPermissionsDialog diff --git a/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/index.tsx b/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/index.tsx index 95ece07c6..9d7fbc258 100644 --- a/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/index.tsx +++ b/packages/components/modules/messages/web/GroupChatDetails/ProfileCard/index.tsx @@ -9,12 +9,13 @@ import { Popover } from '@baseapp-frontend/design-system/components/web/popovers import { usePopover } from '@baseapp-frontend/design-system/hooks/common' import { Box, IconButton, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { ProfileItemFragment$key } from '../../../../../__generated__/ProfileItemFragment.graphql' import { formatHandle } from '../../../../__shared__/common/utils' import { ProfileItemFragment } from '../../../../profiles/common' -import { ADMIN_LABEL, CHAT_ROOM_PARTICIPANT_ROLES } from '../../../common' +import { CHAT_ROOM_PARTICIPANT_ROLES } from '../../../common' import { useChatRoomToggleAdminMutation } from '../../../common/graphql/mutations/ChatRoomToggleAdmin' import AdminOptionsMenu from './AdminOptionsMenu' import MemberOptionsMenu from './MemberOptionsMenu' @@ -28,6 +29,7 @@ const ProfileCard: FC = ({ initiateRemoval, groupId, }) => { + const intl = useIntl() const { id, image, name, urlPath } = useFragment( ProfileItemFragment, groupMember.profile!, @@ -151,13 +153,23 @@ const ProfileCard: FC = ({
{isAdmin && showUrlPath && } - {isAdmin && ADMIN_LABEL} + {isAdmin && + intl.formatMessage({ + id: 'messages.groupChatDetails.profileCard.admin', + defaultMessage: 'Admin', + })} {showMenu ? ( - + diff --git a/packages/components/modules/messages/web/GroupChatDetails/index.tsx b/packages/components/modules/messages/web/GroupChatDetails/index.tsx index 8a07447cf..d31c55746 100644 --- a/packages/components/modules/messages/web/GroupChatDetails/index.tsx +++ b/packages/components/modules/messages/web/GroupChatDetails/index.tsx @@ -6,6 +6,7 @@ import { useCurrentProfile } from '@baseapp-frontend/authentication' import { LoadingState } from '@baseapp-frontend/design-system/components/web/displays' import { Box } from '@mui/material' +import { useIntl } from 'react-intl' import { ConnectionHandler, usePaginationFragment, usePreloadedQuery } from 'react-relay' import { Virtuoso } from 'react-virtuoso' @@ -21,6 +22,7 @@ import { useTitleAndImage, } from '../../common' import LeaveGroupDialog from '../__shared__/LeaveGroupDialog' +import { SHARED_MESSAGES } from '../__shared__/constants' import DefaultBody from './Body' import DefaultHeader from './Header' import DefaultProfileCard from './ProfileCard' @@ -39,6 +41,7 @@ const GroupChatDetails: FC = ({ queryRef, VirtuosoProps = {}, }) => { + const intl = useIntl() const { chatRoom: group } = usePreloadedQuery(GroupDetailsQuery, queryRef) const { currentProfile } = useCurrentProfile() const { image, title } = useTitleAndImage(group) @@ -67,7 +70,7 @@ const GroupChatDetails: FC = ({ ) } diff --git a/packages/components/modules/messages/web/GroupChatEdit/AddMemberCard/index.tsx b/packages/components/modules/messages/web/GroupChatEdit/AddMemberCard/index.tsx index 7f9003ff8..94b6ed1cd 100644 --- a/packages/components/modules/messages/web/GroupChatEdit/AddMemberCard/index.tsx +++ b/packages/components/modules/messages/web/GroupChatEdit/AddMemberCard/index.tsx @@ -5,6 +5,7 @@ import { FC } from 'react' import { AvatarWithPlaceholder } from '@baseapp-frontend/design-system/components/web/avatars' import { Box, Checkbox, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { ProfileItemFragment$key } from '../../../../../__generated__/ProfileItemFragment.graphql' @@ -19,6 +20,7 @@ const AddMemberCard: FC = ({ isBeingAdded = false, isExistingMember = false, }) => { + const intl = useIntl() const { id, image, name, urlPath } = useFragment( ProfileItemFragment, profile as ProfileItemFragment$key, @@ -35,7 +37,10 @@ const AddMemberCard: FC = ({ const getCaptionText = () => { if (isExistingMember) { - return 'Already added to the group' + return intl.formatMessage({ + id: 'messages.groupChatEdit.addMemberCard.alreadyAdded', + defaultMessage: 'Already added to the group', + }) } if (urlPath?.path) { return `@${urlPath.path}` diff --git a/packages/components/modules/messages/web/GroupChatEdit/AddMembersDialog/constants.ts b/packages/components/modules/messages/web/GroupChatEdit/AddMembersDialog/constants.ts index 0897be415..5a4d4a8ff 100644 --- a/packages/components/modules/messages/web/GroupChatEdit/AddMembersDialog/constants.ts +++ b/packages/components/modules/messages/web/GroupChatEdit/AddMembersDialog/constants.ts @@ -1,11 +1,19 @@ +import { IntlShape } from 'react-intl' import z from 'zod' import { CREATE_OR_EDIT_GROUP_FORM_VALUE, DEFAULT_CREATE_OR_EDIT_GROUP_FORM_VALIDATION, } from '../../../common/constants' +import { getCreateOrEditGroupFormValidation } from '../../__shared__/constants' export const DEFAULT_FORM_VALIDATION = z.object({ ...DEFAULT_CREATE_OR_EDIT_GROUP_FORM_VALIDATION.shape, [CREATE_OR_EDIT_GROUP_FORM_VALUE.title]: z.string(), }) + +export const getFormValidation = (intl: IntlShape) => + z.object({ + ...getCreateOrEditGroupFormValidation(intl).shape, + [CREATE_OR_EDIT_GROUP_FORM_VALUE.title]: z.string(), + }) diff --git a/packages/components/modules/messages/web/GroupChatEdit/AddMembersDialog/index.tsx b/packages/components/modules/messages/web/GroupChatEdit/AddMembersDialog/index.tsx index 03a3dc4ec..d9107188f 100644 --- a/packages/components/modules/messages/web/GroupChatEdit/AddMembersDialog/index.tsx +++ b/packages/components/modules/messages/web/GroupChatEdit/AddMembersDialog/index.tsx @@ -8,6 +8,7 @@ import { setFormRelayErrors, useNotification } from '@baseapp-frontend/utils' import { zodResolver } from '@hookform/resolvers/zod' import { LoadingButton } from '@mui/lab' import { useForm } from 'react-hook-form' +import { useIntl } from 'react-intl' import { useAllProfilesList } from '../../../../profiles/common' import { useUpdateChatRoomMutation } from '../../../common' @@ -17,10 +18,11 @@ import { } from '../../../common/constants' import { CreateOrEditGroup } from '../../../common/types' import DefaultGroupChatMembersList from '../../__shared__/GroupChatMembersList' +import { SHARED_MESSAGES } from '../../__shared__/constants' import { ProfileNode } from '../../__shared__/types' import AddMemberCard from '../AddMemberCard' import AddedMemberCard from '../AddedMemberCard' -import { DEFAULT_FORM_VALIDATION } from './constants' +import { getFormValidation } from './constants' import { SearchbarContainer } from './styled' import { AddMembersDialogProps } from './types' @@ -35,6 +37,7 @@ const AddMembersDialog: FC = ({ GroupChatMembersListProps = {}, existingMembers, }) => { + const intl = useIntl() const { sendToast } = useNotification() const boxRef = useRef(null) @@ -46,10 +49,11 @@ const AddMembersDialog: FC = ({ refetch: refetchProfiles, } = useAllProfilesList(allProfilesRef) + const formValidation = useMemo(() => getFormValidation(intl), [intl]) const formReturn = useForm({ defaultValues: DEFAULT_FORM_VALUES, // @ts-ignore TODO: check typing issue with zodResolver - resolver: zodResolver(DEFAULT_FORM_VALIDATION), + resolver: zodResolver(formValidation), mode: 'onBlur', }) @@ -83,7 +87,7 @@ const AddMembersDialog: FC = ({ onCompleted: (response) => { const errors = response?.chatRoomUpdate?.errors if (errors) { - sendToast('Something went wrong', { type: 'error' }) + sendToast(intl.formatMessage(SHARED_MESSAGES.somethingWentWrong), { type: 'error' }) // @ts-ignore TODO: check typing issue with zodResolver setFormRelayErrors(formReturn, errors) } else { @@ -166,7 +170,10 @@ const AddMembersDialog: FC = ({ return ( = ({ disabled={isMutationInFlight || isEditButtonDisabled} loading={isMutationInFlight} > - Confirm + {intl.formatMessage({ id: 'common.confirm', defaultMessage: 'Confirm' })} } onClose={handleClose} diff --git a/packages/components/modules/messages/web/GroupChatEdit/AddMembersMobile/constants.ts b/packages/components/modules/messages/web/GroupChatEdit/AddMembersMobile/constants.ts index 0897be415..5a4d4a8ff 100644 --- a/packages/components/modules/messages/web/GroupChatEdit/AddMembersMobile/constants.ts +++ b/packages/components/modules/messages/web/GroupChatEdit/AddMembersMobile/constants.ts @@ -1,11 +1,19 @@ +import { IntlShape } from 'react-intl' import z from 'zod' import { CREATE_OR_EDIT_GROUP_FORM_VALUE, DEFAULT_CREATE_OR_EDIT_GROUP_FORM_VALIDATION, } from '../../../common/constants' +import { getCreateOrEditGroupFormValidation } from '../../__shared__/constants' export const DEFAULT_FORM_VALIDATION = z.object({ ...DEFAULT_CREATE_OR_EDIT_GROUP_FORM_VALIDATION.shape, [CREATE_OR_EDIT_GROUP_FORM_VALUE.title]: z.string(), }) + +export const getFormValidation = (intl: IntlShape) => + z.object({ + ...getCreateOrEditGroupFormValidation(intl).shape, + [CREATE_OR_EDIT_GROUP_FORM_VALUE.title]: z.string(), + }) diff --git a/packages/components/modules/messages/web/GroupChatEdit/AddMembersMobile/index.tsx b/packages/components/modules/messages/web/GroupChatEdit/AddMembersMobile/index.tsx index d08f7aac1..9fa4ecaf8 100644 --- a/packages/components/modules/messages/web/GroupChatEdit/AddMembersMobile/index.tsx +++ b/packages/components/modules/messages/web/GroupChatEdit/AddMembersMobile/index.tsx @@ -10,6 +10,7 @@ import { setFormRelayErrors, useNotification } from '@baseapp-frontend/utils' import { zodResolver } from '@hookform/resolvers/zod' import { Box, Typography } from '@mui/material' import { useForm } from 'react-hook-form' +import { useIntl } from 'react-intl' import { useAllProfilesList } from '../../../../profiles/common' import { useUpdateChatRoomMutation } from '../../../common' @@ -19,11 +20,12 @@ import { } from '../../../common/constants' import { CreateOrEditGroup } from '../../../common/types' import DefaultGroupChatMembersList from '../../__shared__/GroupChatMembersList' +import { SHARED_MESSAGES } from '../../__shared__/constants' import { ProfileNode } from '../../__shared__/types' import AddMemberCard from '../AddMemberCard' import AddedMemberCard from '../AddedMemberCard' import { Container as HeaderContainer } from '../Header/styled' -import { DEFAULT_FORM_VALIDATION } from './constants' +import { getFormValidation } from './constants' import { SearchbarContainer } from './styled' import { AddMembersMobileProps } from './types' @@ -37,6 +39,7 @@ const AddMembersMobile: FC = ({ GroupChatMembersListProps = {}, existingMembers = [], }) => { + const intl = useIntl() const { sendToast } = useNotification() const { @@ -47,10 +50,11 @@ const AddMembersMobile: FC = ({ refetch: refetchProfiles, } = useAllProfilesList(allProfilesRef) + const formValidation = useMemo(() => getFormValidation(intl), [intl]) const formReturn = useForm({ defaultValues: DEFAULT_FORM_VALUES, // @ts-ignore TODO: check typing issue with zodResolver - resolver: zodResolver(DEFAULT_FORM_VALIDATION), + resolver: zodResolver(formValidation), mode: 'onBlur', }) @@ -84,7 +88,7 @@ const AddMembersMobile: FC = ({ onCompleted: (response) => { const errors = response?.chatRoomUpdate?.errors if (errors) { - sendToast('Something went wrong', { type: 'error' }) + sendToast(intl.formatMessage(SHARED_MESSAGES.somethingWentWrong), { type: 'error' }) // @ts-ignore TODO: fix typing issues setFormRelayErrors(formReturn, errors) } else { @@ -132,14 +136,20 @@ const AddMembersMobile: FC = ({ return ( - + - Add Member + {intl.formatMessage(SHARED_MESSAGES.addMember)} { diff --git a/packages/components/modules/messages/web/GroupChatEdit/Header/index.tsx b/packages/components/modules/messages/web/GroupChatEdit/Header/index.tsx index ae046e935..c885e8d02 100644 --- a/packages/components/modules/messages/web/GroupChatEdit/Header/index.tsx +++ b/packages/components/modules/messages/web/GroupChatEdit/Header/index.tsx @@ -9,6 +9,7 @@ import { } from '@baseapp-frontend/design-system/components/web/icons' import { Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { Container } from './styled' import { HeaderProps } from './types' @@ -20,27 +21,44 @@ const Header: FC = ({ isMutationInFlight, onCancellation, onSubmit, - title = 'Edit Group', + title, titleProps = {}, -}) => ( - - - - - - {title} - - { - onSubmit() - }} - > - - - -) +}) => { + const intl = useIntl() + + return ( + + + + + + {title ?? + intl.formatMessage({ + id: 'messages.groupChatEdit.header.title', + defaultMessage: 'Edit Group', + })} + + { + onSubmit() + }} + > + + + + ) +} export default Header diff --git a/packages/components/modules/messages/web/GroupChatEdit/constants.ts b/packages/components/modules/messages/web/GroupChatEdit/constants.ts index 92f516530..2f172f073 100644 --- a/packages/components/modules/messages/web/GroupChatEdit/constants.ts +++ b/packages/components/modules/messages/web/GroupChatEdit/constants.ts @@ -1,3 +1,4 @@ +import { IntlShape } from 'react-intl' import z from 'zod' import { @@ -5,6 +6,7 @@ import { DEFAULT_CREATE_OR_EDIT_GROUP_FORM_VALIDATION, } from '../../common/constants' import { CreateOrEditGroup } from '../../common/types' +import { getCreateOrEditGroupFormValidation } from '../__shared__/constants' export const getDefaultFormValues = ( title: string, @@ -20,3 +22,9 @@ export const DEFAULT_FORM_VALIDATION = z.object({ ...DEFAULT_CREATE_OR_EDIT_GROUP_FORM_VALIDATION.shape, [CREATE_OR_EDIT_GROUP_FORM_VALUE.participants]: z.any(), }) as any // TODO: fix typing issue with zodResolver + +export const getFormValidation = (intl: IntlShape) => + z.object({ + ...getCreateOrEditGroupFormValidation(intl).shape, + [CREATE_OR_EDIT_GROUP_FORM_VALUE.participants]: z.any(), + }) as any // TODO: fix typing issue with zodResolver diff --git a/packages/components/modules/messages/web/GroupChatEdit/index.tsx b/packages/components/modules/messages/web/GroupChatEdit/index.tsx index b23ff0187..7eda32bb3 100644 --- a/packages/components/modules/messages/web/GroupChatEdit/index.tsx +++ b/packages/components/modules/messages/web/GroupChatEdit/index.tsx @@ -9,6 +9,7 @@ import { filterDirtyValues, setFormRelayErrors, useNotification } from '@baseapp import { zodResolver } from '@hookform/resolvers/zod' import { Box } from '@mui/material' import { useForm } from 'react-hook-form' +import { useIntl } from 'react-intl' import { usePaginationFragment, usePreloadedQuery } from 'react-relay' import { ChatRoomParticipantsPaginationQuery } from '../../../../__generated__/ChatRoomParticipantsPaginationQuery.graphql' @@ -29,10 +30,11 @@ import { CreateOrEditGroup } from '../../common/types' import EditGroupTitleAndImage from '../__shared__/EditGroupTitleAndImage' import DefaultGroupChatMembersList from '../__shared__/GroupChatMembersList' import LeaveGroupDialog from '../__shared__/LeaveGroupDialog' +import { SHARED_MESSAGES } from '../__shared__/constants' import AddMembersDialog from './AddMembersDialog' import AddMembersMobile from './AddMembersMobile' import DefaultHeader from './Header' -import { DEFAULT_FORM_VALIDATION, getDefaultFormValues } from './constants' +import { getDefaultFormValues, getFormValidation } from './constants' import { GroupChatEditProps } from './types' const GroupChatEdit: FC = ({ @@ -48,6 +50,7 @@ const GroupChatEdit: FC = ({ queryRef, roomId, }) => { + const intl = useIntl() const { sendToast } = useNotification() const [open, setOpen] = useState(false) const [memberToRemove, setMemberToRemove] = useState(null) @@ -76,9 +79,10 @@ const GroupChatEdit: FC = ({ [membersList], ) const { isSoleAdmin } = useCheckIsAdmin(membersList?.participants) + const formValidation = useMemo(() => getFormValidation(intl), [intl]) const formReturn = useForm({ defaultValues: getDefaultFormValues(title || '', chatImage), - resolver: zodResolver(DEFAULT_FORM_VALIDATION), + resolver: zodResolver(formValidation), mode: 'onBlur', }) @@ -120,7 +124,7 @@ const GroupChatEdit: FC = ({ onCompleted: (response) => { const errors = response?.chatRoomUpdate?.errors if (errors) { - sendToast('Something went wrong', { type: 'error' }) + sendToast(intl.formatMessage(SHARED_MESSAGES.somethingWentWrong), { type: 'error' }) setFormRelayErrors(formReturn, errors) } else { onValidSubmission() diff --git a/packages/components/modules/messages/web/MessageUpdate/index.tsx b/packages/components/modules/messages/web/MessageUpdate/index.tsx index b3b5f99fe..4ed7048e9 100644 --- a/packages/components/modules/messages/web/MessageUpdate/index.tsx +++ b/packages/components/modules/messages/web/MessageUpdate/index.tsx @@ -6,6 +6,7 @@ import { setFormRelayErrors } from '@baseapp-frontend/utils' import { zodResolver } from '@hookform/resolvers/zod' import { useForm } from 'react-hook-form' +import { useIntl } from 'react-intl' import { SOCIAL_UPSERT_FORM, @@ -83,6 +84,7 @@ const MessageUpdate: FC = ({ SocialInputProps = {}, disableMentions = true, }) => { + const intl = useIntl() const inputRef = useRef(null) const form = useForm({ @@ -158,8 +160,14 @@ const MessageUpdate: FC = ({ handleEditCancel, formId: 'message-update', disabled: isMutationInFlight, - ariaLabel: 'save message edit', - cancelAriaLabel: 'cancel message edit', + ariaLabel: intl.formatMessage({ + id: 'messages.messageUpdate.saveAriaLabel', + defaultMessage: 'save message edit', + }), + cancelAriaLabel: intl.formatMessage({ + id: 'messages.messageUpdate.cancelAriaLabel', + defaultMessage: 'cancel message edit', + }), }} {...mergedSocialInputProps} /> diff --git a/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/constants.ts b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/constants.ts new file mode 100644 index 000000000..452ec90f4 --- /dev/null +++ b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/constants.ts @@ -0,0 +1,42 @@ +import { defineMessages } from 'react-intl' + +export const MESSAGE_ITEM_MESSAGES = defineMessages({ + itemTitle: { + id: 'messages.messageItem.itemTitle', + defaultMessage: 'message', + }, + deleted: { + id: 'messages.messageItem.deletedToast', + defaultMessage: 'Your message was deleted', + }, + copied: { + id: 'messages.messageItem.copiedToast', + defaultMessage: 'Message copied to clipboard.', + }, + copy: { + id: 'messages.messageItem.actions.copy', + defaultMessage: 'Copy', + }, + edit: { + id: 'common.edit', + defaultMessage: 'Edit', + }, + downloadAttachments: { + id: 'messages.messageItem.actions.downloadAttachments', + defaultMessage: 'Download Attachments', + }, + deleteDialogTitle: { + id: 'messages.messageItem.deleteDialog.title', + defaultMessage: 'Delete message?', + }, + deleteDialogContentGroup: { + id: 'messages.messageItem.deleteDialog.contentGroup', + defaultMessage: + 'Are you sure you want to delete this message? The message will be deleted for everyone in this chat.', + }, + deleteDialogContentDirect: { + id: 'messages.messageItem.deleteDialog.contentDirect', + defaultMessage: + 'Are you sure you want to delete this message? The message will be deleted for both you and the other person.', + }, +}) diff --git a/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/index.tsx b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/index.tsx index daa15ef6c..7dde99c0c 100644 --- a/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/index.tsx +++ b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/MessageItem/index.tsx @@ -10,12 +10,14 @@ import { import { useNotification } from '@baseapp-frontend/utils' import { Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { ActionsOverlay, HOVER_OVERLAY_MODES } from '../../../../../../__shared__/web' import { MessageItemFragment } from '../../../../../common' import { useMessageDeleteMutation } from '../../../../../common/graphql/mutations/MessageDelete' import MessageUpdate from '../../../../MessageUpdate' +import { MESSAGE_ITEM_MESSAGES } from './constants' import { MessageContent, MessageItemContainer } from './styled' import { MessageItemProps } from './types' @@ -24,6 +26,7 @@ const MessageItem: FC = ({ isFirstGroupedMessage, isGroup = false, }) => { + const intl = useIntl() const { currentProfile } = useCurrentProfile() const message = useFragment(MessageItemFragment, messageRef) const isOwnMessage = currentProfile?.id === message?.profile?.id @@ -46,15 +49,17 @@ const MessageItem: FC = ({ }, onCompleted: (response, errors) => { if (!errors) { - sendToast('Your message was deleted', { type: 'error' }) + sendToast(intl.formatMessage(MESSAGE_ITEM_MESSAGES.deleted), { type: 'error' }) } }, }) } - const deleteDialogContent = isGroup - ? 'The message will be deleted for everyone in this chat.' - : 'The message will be deleted for both you and the other person.' + const deleteDialogContent = intl.formatMessage( + isGroup + ? MESSAGE_ITEM_MESSAGES.deleteDialogContentGroup + : MESSAGE_ITEM_MESSAGES.deleteDialogContentDirect, + ) const renderMessageContent = () => { if (isEditMode) { @@ -74,22 +79,25 @@ const MessageItem: FC = ({ return ( , - label: 'Copy', + label: intl.formatMessage(MESSAGE_ITEM_MESSAGES.copy), onClick: () => { navigator.clipboard.writeText(message?.content || '') - sendToast('Message copied to clipboard.', { type: 'info', shouldShowProgress: true }) + sendToast(intl.formatMessage(MESSAGE_ITEM_MESSAGES.copied), { + type: 'info', + shouldShowProgress: true, + }) }, hasPermission: true, }, { disabled: deletedMessage || !isOwnMessage, icon: , - label: 'Edit', + label: intl.formatMessage(MESSAGE_ITEM_MESSAGES.edit), onClick: () => { setIsEditMode(true) }, @@ -99,7 +107,7 @@ const MessageItem: FC = ({ { disabled: false, icon: , - label: 'Download Attachments', + label: intl.formatMessage(MESSAGE_ITEM_MESSAGES.downloadAttachments), onClick: () => {}, // TODO: Implement download attachments hasPermission: true, }, @@ -110,7 +118,8 @@ const MessageItem: FC = ({ isDeletingItem={isMutationInFlight} disableDeleteButton={!isOwnMessage || deletedMessage} DeleteDialogProps={{ - content: `Are you sure you want to delete this message? ${deleteDialogContent}`, + title: intl.formatMessage(MESSAGE_ITEM_MESSAGES.deleteDialogTitle), + content: deleteDialogContent, }} ContainerProps={{ flexDirection: isOwnMessage ? 'row' : 'row-reverse', diff --git a/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/index.tsx b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/index.tsx index f3a8ee8cd..ead7fa10b 100644 --- a/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/index.tsx +++ b/packages/components/modules/messages/web/MessagesList/MessagesGroup/UserMessage/index.tsx @@ -5,8 +5,10 @@ import { AvatarWithPlaceholder } from '@baseapp-frontend/design-system/component import { Box, Typography } from '@mui/material' import { DateTime } from 'luxon' +import { useIntl } from 'react-intl' import { MAXIMUM_DIFF_TO_GROUP_MESSAGES_CREATED_TIME } from '../../../../common' +import { SHARED_MESSAGES } from '../../../__shared__/constants' import DefaultMessageItem from './MessageItem' import Timestamp from './Timestamp' import { MessageColumn, MessageRow } from './styled' @@ -21,6 +23,7 @@ const UserMessage: FC = ({ MessageItem = DefaultMessageItem, MessageItemProps = {}, }) => { + const intl = useIntl() const { currentProfile } = useCurrentProfile() const isProfileNullOrUndefined = message?.profile == null || message?.profile === undefined @@ -86,7 +89,9 @@ const UserMessage: FC = ({ {canShowName && ( - {isProfileNullOrUndefined ? 'Deleted User' : message?.profile?.name} + {isProfileNullOrUndefined + ? intl.formatMessage(SHARED_MESSAGES.deletedUser) + : message?.profile?.name} )} = ({ UserMessageProps = {}, }) => { const theme = useTheme() + const intl = useIntl() const renderDateOnTopOfMessagesGroup = useCallback( (index: number) => { @@ -38,14 +41,14 @@ const MessagesGroup: FC = ({ ) { return ( - {displayFormattedDate(currentMessage?.created)} + {displayFormattedDate(intl, currentMessage?.created)} ) } return null }, - [allMessages, allMessagesLastIndex, hasNext], + [allMessages, allMessagesLastIndex, hasNext, intl], ) const renderUnreadMessagesDivider = useCallback( @@ -63,7 +66,7 @@ const MessagesGroup: FC = ({ }} > - New Messages + {intl.formatMessage(MESSAGES_GROUP_MESSAGES.newMessages)} ) @@ -71,7 +74,7 @@ const MessagesGroup: FC = ({ return null }, - [allMessages, firstUnreadMessageId, theme.palette.error.light], + [allMessages, firstUnreadMessageId, theme.palette.error.light, intl], ) if (!message) return null diff --git a/packages/components/modules/messages/web/MessagesList/MessagesGroup/utils.ts b/packages/components/modules/messages/web/MessagesList/MessagesGroup/utils.ts index 7c8c94d34..425e57dac 100644 --- a/packages/components/modules/messages/web/MessagesList/MessagesGroup/utils.ts +++ b/packages/components/modules/messages/web/MessagesList/MessagesGroup/utils.ts @@ -1,7 +1,12 @@ import { DATE_FORMAT, formatDateFromApi, isToday, isYesterday } from '@baseapp-frontend/utils' -export const displayFormattedDate = (date: string) => { - if (isToday(date)) return 'Today' - if (isYesterday(date)) return 'Yesterday' +import { IntlShape } from 'react-intl' + +import { SHARED_MESSAGES } from '../../__shared__/constants' +import { MESSAGES_GROUP_MESSAGES } from './constants' + +export const displayFormattedDate = (intl: IntlShape, date: string) => { + if (isToday(date)) return intl.formatMessage(MESSAGES_GROUP_MESSAGES.today) + if (isYesterday(date)) return intl.formatMessage(SHARED_MESSAGES.yesterday) return formatDateFromApi(date, { toFormat: DATE_FORMAT[2] }) } diff --git a/packages/components/modules/messages/web/MessagesList/index.tsx b/packages/components/modules/messages/web/MessagesList/index.tsx index 626c3cda6..cd49cb6f9 100644 --- a/packages/components/modules/messages/web/MessagesList/index.tsx +++ b/packages/components/modules/messages/web/MessagesList/index.tsx @@ -4,6 +4,7 @@ import { useCurrentProfile } from '@baseapp-frontend/authentication' import { LoadingState } from '@baseapp-frontend/design-system/components/web/displays' import { Box } from '@mui/material' +import { useIntl } from 'react-intl' import { usePaginationFragment } from 'react-relay' import { Virtuoso, VirtuosoHandle } from 'react-virtuoso' @@ -27,6 +28,7 @@ const MessagesList: FC = ({ MessagesGroup = DefaultMessagesGroup, MessagesGroupProps = {}, }) => { + const intl = useIntl() const { data: room, loadNext, @@ -90,7 +92,10 @@ const MessagesList: FC = ({ ) } diff --git a/packages/components/modules/messages/web/ProfileSummary/Body/index.tsx b/packages/components/modules/messages/web/ProfileSummary/Body/index.tsx index d0f7b8d01..ddb39de82 100644 --- a/packages/components/modules/messages/web/ProfileSummary/Body/index.tsx +++ b/packages/components/modules/messages/web/ProfileSummary/Body/index.tsx @@ -9,10 +9,12 @@ import { import { TypographyWithEllipsis } from '@baseapp-frontend/design-system/components/web/typographies' import { Box, Divider, Typography } from '@mui/material' +import { FormattedMessage, useIntl } from 'react-intl' import { formatHandle } from '../../../../__shared__/common/utils' import { useSingleChatDetails } from '../../../common' import AddContactToGroupDialog from '../../AddContactToGroupDialog' +import { SHARED_MESSAGES } from '../../__shared__/constants' import { ButtonContainer, HeaderContainer, @@ -23,7 +25,8 @@ import { import { BodyProps } from './types' const Body: FC = ({ avatarSize = 144, chatRoomRef }) => { - const { title, image, username, biography, id } = useSingleChatDetails(chatRoomRef) + const intl = useIntl() + const { title, image, username, biography, id, isDeletedUser } = useSingleChatDetails(chatRoomRef) const [isAddToGroupOpen, setIsAddToGroupOpen] = useState(false) const profilePath = username ?? (id ? `/profile/${id}` : undefined) @@ -39,7 +42,7 @@ const Body: FC = ({ avatarSize = 144, chatRoomRef }) => { /> - {title} + {isDeletedUser ? intl.formatMessage(SHARED_MESSAGES.deletedUser) : title} {username && ( @@ -52,26 +55,38 @@ const Body: FC = ({ avatarSize = 144, chatRoomRef }) => { window.open(profilePath, '_blank', 'noopener,noreferrer')} disabled={!profilePath} > - Go to profile + setIsAddToGroupOpen(true)} disabled={!id} > - Add contact to a group + @@ -84,7 +99,7 @@ const Body: FC = ({ avatarSize = 144, chatRoomRef }) => { )} - About + diff --git a/packages/components/modules/messages/web/ProfileSummary/Header/index.tsx b/packages/components/modules/messages/web/ProfileSummary/Header/index.tsx index 0dbfb7898..8fc8a0392 100644 --- a/packages/components/modules/messages/web/ProfileSummary/Header/index.tsx +++ b/packages/components/modules/messages/web/ProfileSummary/Header/index.tsx @@ -4,7 +4,9 @@ import { IconButton } from '@baseapp-frontend/design-system/components/web/butto import { Iconify } from '@baseapp-frontend/design-system/components/web/images' import { Typography } from '@mui/material' +import { useIntl } from 'react-intl' +import { SHARED_MESSAGES } from '../../__shared__/constants' import { ProfileSummaryHeaderContainer } from './styled' import { HeaderProps } from './types' @@ -12,19 +14,23 @@ const Header: FC = ({ backIcon = 'eva:arrow-ios-back-fill', backIconProps = {}, onBackButtonClicked, -}) => ( - - - - - - Contact Details - - -) +}) => { + const intl = useIntl() + + return ( + + + + + + {intl.formatMessage(SHARED_MESSAGES.contactDetails)} + + + ) +} export default Header 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/modules/messages/web/SendMessage/index.tsx b/packages/components/modules/messages/web/SendMessage/index.tsx index 968bd01d7..634a6b8c7 100644 --- a/packages/components/modules/messages/web/SendMessage/index.tsx +++ b/packages/components/modules/messages/web/SendMessage/index.tsx @@ -8,6 +8,7 @@ import { setFormRelayErrors, useNotification } from '@baseapp-frontend/utils' import { zodResolver } from '@hookform/resolvers/zod' import { Box } from '@mui/material' import { useForm } from 'react-hook-form' +import { useIntl } from 'react-intl' import { ConnectionHandler } from 'react-relay' import { @@ -85,8 +86,17 @@ const SendMessage = forwardRef( { roomId, SocialInput = DefaultSocialInput, SocialInputProps = {}, disableMentions = true }, ref, ) => { + const intl = useIntl() const { currentProfile } = useCurrentProfile() const { sendToast } = useNotification() + const sendErrorToast = () => + sendToast( + intl.formatMessage({ + id: 'messages.sendMessage.errorToast', + defaultMessage: 'Your last message could not be sent. Please try again.', + }), + { type: 'error' }, + ) const form = useForm({ defaultValues: DEFAULT_SOCIAL_UPSERT_FORM_VALUES, @@ -155,18 +165,16 @@ const SendMessage = forwardRef( onCompleted: (response, errors) => { if (errors) { // TODO: handle errors - sendToast('Your last message could not be sent. Please try again.', { type: 'error' }) + sendErrorToast() } const mutationErrors = response?.chatRoomSendMessage?.errors if (mutationErrors?.length) { setFormRelayErrors(form, mutationErrors) - sendToast('Your last message could not be sent. Please try again.', { type: 'error' }) + sendErrorToast() } }, // TODO: handle errors - onError: () => { - sendToast('Your last message could not be sent. Please try again.', { type: 'error' }) - }, + onError: sendErrorToast, }) form.reset() clearDraft() diff --git a/packages/components/modules/messages/web/__shared__/EditGroupTitleAndImage/index.tsx b/packages/components/modules/messages/web/__shared__/EditGroupTitleAndImage/index.tsx index 81e8578b1..01724e6c1 100644 --- a/packages/components/modules/messages/web/__shared__/EditGroupTitleAndImage/index.tsx +++ b/packages/components/modules/messages/web/__shared__/EditGroupTitleAndImage/index.tsx @@ -5,7 +5,9 @@ import { FileUploadButton } from '@baseapp-frontend/design-system/components/web import { TextField } from '@baseapp-frontend/design-system/components/web/inputs' import { Box, Button, Typography, useTheme } from '@mui/material' +import { useIntl } from 'react-intl' +import { SHARED_MESSAGES } from '../constants' import { DEFAULT_IMAGE_FORMATS, DEFAULT_IMAGE_MAX_SIZE } from './constants' import { ErrorContainer, UploadImageContainer } from './styled' import { EditGroupTitleAndImageProps } from './types' @@ -16,6 +18,7 @@ const EditGroupTitleAndImage: FC = ({ FORM_VALUE, isMutationInFlight, }) => { + const intl = useIntl() const { control, setValue, watch, getFieldState, clearErrors, trigger } = form const handleRemoveImage = () => { @@ -56,7 +59,17 @@ const EditGroupTitleAndImage: FC = ({ setFile={setValue} accept={DEFAULT_IMAGE_FORMATS} maxSize={DEFAULT_IMAGE_MAX_SIZE} - label={watchImage ? 'Change Avatar' : 'Upload Avatar'} + label={ + watchImage + ? intl.formatMessage({ + id: 'messages.editGroupTitleAndImage.changeAvatar', + defaultMessage: 'Change Avatar', + }) + : intl.formatMessage({ + id: 'messages.editGroupTitleAndImage.uploadAvatar', + defaultMessage: 'Upload Avatar', + }) + } /> {watchImage && ( )} ( - Search for profiles + - Once you connect to other profiles they will be shown here. + diff --git a/packages/components/modules/messages/web/__shared__/GroupChatMembersList/ProfileCard/index.tsx b/packages/components/modules/messages/web/__shared__/GroupChatMembersList/ProfileCard/index.tsx index 1d5301467..116d44c63 100644 --- a/packages/components/modules/messages/web/__shared__/GroupChatMembersList/ProfileCard/index.tsx +++ b/packages/components/modules/messages/web/__shared__/GroupChatMembersList/ProfileCard/index.tsx @@ -5,11 +5,13 @@ import { FC } from 'react' import { AvatarWithPlaceholder } from '@baseapp-frontend/design-system/components/web/avatars' import { Box, Button, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' import { useFragment } from 'react-relay' import { ProfileItemFragment$key } from '../../../../../../__generated__/ProfileItemFragment.graphql' import { formatHandle } from '../../../../../__shared__/common/utils' import { ProfileItemFragment } from '../../../../../profiles/common' +import { SHARED_MESSAGES } from '../../constants' import { MainContainer } from './styled' import { ProfileCardProps } from './types' @@ -52,7 +54,11 @@ const ProfileCard: FC = ({ sx={{ maxWidth: 'fit-content', justifySelf: 'end' }} > - {isMember ? 'Remove' : 'Add'} + {isMember ? ( + + ) : ( + + )} diff --git a/packages/components/modules/messages/web/__shared__/GroupChatMembersList/ProfilesList/index.tsx b/packages/components/modules/messages/web/__shared__/GroupChatMembersList/ProfilesList/index.tsx index c9099ba0e..a69685d60 100644 --- a/packages/components/modules/messages/web/__shared__/GroupChatMembersList/ProfilesList/index.tsx +++ b/packages/components/modules/messages/web/__shared__/GroupChatMembersList/ProfilesList/index.tsx @@ -6,10 +6,12 @@ import { AvatarButton } from '@baseapp-frontend/design-system/components/web/but import { LoadingState } from '@baseapp-frontend/design-system/components/web/displays' import { Box, Typography, useTheme } from '@mui/material' +import { useIntl } from 'react-intl' import { Virtuoso } from 'react-virtuoso' import { SearchNotFoundState as DefaultSearchNotFoundState } from '../../../../../__shared__/web' import DefaultEmptyProfilesListState from '../../EmptyProfilesListState' +import { SHARED_MESSAGES } from '../../constants' import { ProfileNode } from '../../types' import { ProfilesListProps } from './types' @@ -23,8 +25,8 @@ const ProfilesList: FC = ({ renderItem, VirtuosoProps = {}, NormalListProps = {}, - label = 'Available connections', - title = 'Connections', + label, + title, EmptyProfilesListState = DefaultEmptyProfilesListState, SearchNotFoundState = DefaultSearchNotFoundState, allowAddMember = false, @@ -32,6 +34,19 @@ const ProfilesList: FC = ({ removeTitle = false, }) => { const theme = useTheme() + const intl = useIntl() + const menuLabel = + label ?? + intl.formatMessage({ + id: 'messages.groupMembersList.profilesList.label', + defaultMessage: 'Available connections', + }) + const menuTitle = + title ?? + intl.formatMessage({ + id: 'messages.groupMembersList.profilesList.title', + defaultMessage: 'Connections', + }) const renderLoadingState = () => { if (!isLoadingNext) return @@ -39,7 +54,7 @@ const ProfilesList: FC = ({ ) } @@ -52,21 +67,26 @@ const ProfilesList: FC = ({ return ( <> - + {!removeTitle && ( - {title} + {menuTitle} )} - {allowAddMember && } + {allowAddMember && ( + + )} {isPaginated ? ( = ({ MembersList = DefaultProfilesList, MembersListProps = {}, }) => { + const intl = useIntl() const [isPending, startTransition] = useTransition() const { control: searchControl, @@ -119,8 +122,11 @@ const GroupChatMembersList: FC = ({ loadNext={membersLoadNext} hasNext={membersHasNext} isLoadingNext={membersIsLoadingNext} - label="Selected group members" - title="Members" + label={intl.formatMessage({ + id: 'messages.groupMembersList.selectedMembersLabel', + defaultMessage: 'Selected group members', + })} + title={intl.formatMessage(SHARED_MESSAGES.members)} {...MembersListProps} /> {connections && ( diff --git a/packages/components/modules/messages/web/__shared__/LeaveGroupDialog/constants.ts b/packages/components/modules/messages/web/__shared__/LeaveGroupDialog/constants.ts new file mode 100644 index 000000000..2b18e5c15 --- /dev/null +++ b/packages/components/modules/messages/web/__shared__/LeaveGroupDialog/constants.ts @@ -0,0 +1,61 @@ +import { defineMessages } from 'react-intl' + +import { + LEAVE_GROUP_DIALOG_TEXT_COPY_ACTION_KEYS, + LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS, +} from '../../../common' + +export const LEAVE_GROUP_DIALOG_MESSAGES = defineMessages({ + removeNamedTitle: { + id: 'messages.leaveGroupDialog.removeNamed.title', + defaultMessage: 'Remove {name}?', + }, + leaveGroupButton: { + id: 'messages.leaveGroupDialog.leaveGroupButton', + defaultMessage: 'Leave group', + }, + removeSuccess: { + id: 'messages.leaveGroupDialog.removeSuccess', + defaultMessage: 'Member was successfully removed', + }, +}) + +export const LEAVE_GROUP_DIALOG_COPY_MESSAGES = { + [LEAVE_GROUP_DIALOG_TEXT_COPY_ACTION_KEYS.IS_LEAVING]: { + [LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS.ADMIN]: defineMessages({ + title: { + id: 'messages.leaveGroupDialog.leaveAsSoleAdmin.title', + defaultMessage: 'Leave without choosing an admin?', + }, + content: { + id: 'messages.leaveGroupDialog.leaveAsSoleAdmin.content', + defaultMessage: + 'You can choose a new admin from the people listed under members. If you leave the group without choosing a new admin, the most senior group member will become admin.', + }, + }), + [LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS.MEMBER]: defineMessages({ + title: { + id: 'messages.leaveGroupDialog.leave.title', + defaultMessage: 'Leave group chat?', + }, + content: { + id: 'messages.leaveGroupDialog.leave.content', + defaultMessage: + 'You will stop receiving messages from this conversation and people will see that you left.', + }, + }), + }, + [LEAVE_GROUP_DIALOG_TEXT_COPY_ACTION_KEYS.IS_REMOVING]: { + [LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS.ADMIN]: defineMessages({ + title: { + id: 'messages.leaveGroupDialog.remove.title', + defaultMessage: 'Remove group member?', + }, + content: { + id: 'messages.leaveGroupDialog.remove.content', + defaultMessage: + 'Are you sure you want to remove this person from the conversation? They will no longer be able to send or receive new messages.', + }, + }), + }, +} as const diff --git a/packages/components/modules/messages/web/__shared__/LeaveGroupDialog/index.tsx b/packages/components/modules/messages/web/__shared__/LeaveGroupDialog/index.tsx index 6c1a5c9de..ac9d9bf48 100644 --- a/packages/components/modules/messages/web/__shared__/LeaveGroupDialog/index.tsx +++ b/packages/components/modules/messages/web/__shared__/LeaveGroupDialog/index.tsx @@ -5,10 +5,17 @@ import { FC } from 'react' import { ConfirmDialog } from '@baseapp-frontend/design-system/components/web/dialogs' import { LoadingButton } from '@mui/lab' +import { useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { ProfileItemFragment } from '../../../../profiles/common' -import { LEAVE_GROUP_DIALOG_TEXT_COPY_TYPE_KEYS, useLeaveGroup } from '../../../common' +import { + LEAVE_GROUP_DIALOG_TEXT_COPY_ACTION_KEYS, + LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS, + useLeaveGroup, +} from '../../../common' +import { SHARED_MESSAGES } from '../constants' +import { LEAVE_GROUP_DIALOG_COPY_MESSAGES, LEAVE_GROUP_DIALOG_MESSAGES } from './constants' import { LeaveGroupDialogProps } from './types' const LeaveGroupDialog: FC = ({ @@ -21,35 +28,46 @@ const LeaveGroupDialog: FC = ({ roomId, isSoleAdmin = false, }) => { + const intl = useIntl() const removingParticipantData = useFragment( ProfileItemFragment, removingParticipantFragmentRef ?? null, ) - const { getLeaveGroupDialogTextCopy, onRemoveConfirmed, isMutationInFlight } = useLeaveGroup({ + const { getLeaveGroupDialogTextCopyKeys, onRemoveConfirmed, isMutationInFlight } = useLeaveGroup({ profileId, removingParticipantId: removingParticipantData?.id ?? profileId, roomId, isSoleAdmin, onClose, + removeSuccessMessage: intl.formatMessage(LEAVE_GROUP_DIALOG_MESSAGES.removeSuccess), }) + const getCopyMessages = () => { + const { action, role } = getLeaveGroupDialogTextCopyKeys() + return action === LEAVE_GROUP_DIALOG_TEXT_COPY_ACTION_KEYS.IS_LEAVING + ? LEAVE_GROUP_DIALOG_COPY_MESSAGES[action][role] + : LEAVE_GROUP_DIALOG_COPY_MESSAGES[action][LEAVE_GROUP_DIALOG_TEXT_COPY_ROLE_KEYS.ADMIN] + } + const getTitle = () => { if (customTitle) return customTitle // If the member being removed is NOT the current user's profile, // and we have access to that member's name data, display their actual name if (profileId !== removingParticipantData?.id && removingParticipantData?.name) { - return `Remove ${removingParticipantData.name}?` + return intl.formatMessage(LEAVE_GROUP_DIALOG_MESSAGES.removeNamedTitle, { + name: removingParticipantData.name, + }) } - return getLeaveGroupDialogTextCopy(LEAVE_GROUP_DIALOG_TEXT_COPY_TYPE_KEYS.TITLE) + return intl.formatMessage(getCopyMessages().title) } const getContent = () => { if (customContent) return customContent - return getLeaveGroupDialogTextCopy(LEAVE_GROUP_DIALOG_TEXT_COPY_TYPE_KEYS.CONTENT) + return intl.formatMessage(getCopyMessages().content) } return ( @@ -63,7 +81,11 @@ const LeaveGroupDialog: FC = ({ disabled={isMutationInFlight} loading={isMutationInFlight} > - {removingParticipantData?.id === profileId ? 'Leave group' : 'Remove'} + {intl.formatMessage( + removingParticipantData?.id === profileId + ? LEAVE_GROUP_DIALOG_MESSAGES.leaveGroupButton + : SHARED_MESSAGES.remove, + )} } onClose={onClose} diff --git a/packages/components/modules/messages/web/__shared__/constants.ts b/packages/components/modules/messages/web/__shared__/constants.ts new file mode 100644 index 000000000..2163f7102 --- /dev/null +++ b/packages/components/modules/messages/web/__shared__/constants.ts @@ -0,0 +1,93 @@ +import { IntlShape, defineMessages } from 'react-intl' +import z from 'zod' + +import { + CREATE_OR_EDIT_GROUP_FORM_VALUE, + DEFAULT_CREATE_OR_EDIT_GROUP_FORM_VALIDATION, +} from '../../common/constants' + +export const SHARED_MESSAGES = defineMessages({ + somethingWentWrong: { + id: 'messages.shared.somethingWentWrong', + defaultMessage: 'Something went wrong', + }, + memberCount: { + id: 'messages.shared.memberCount', + defaultMessage: '{count, plural, one {# member} other {# members}}', + }, + deletedUser: { + id: 'messages.shared.deletedUser', + defaultMessage: 'Deleted User', + }, + yesterday: { + id: 'messages.shared.yesterday', + defaultMessage: 'Yesterday', + }, + archiveChat: { + id: 'messages.shared.archiveChat', + defaultMessage: 'Archive Chat', + }, + unarchiveChat: { + id: 'messages.shared.unarchiveChat', + defaultMessage: 'Unarchive Chat', + }, + returnToChatRooms: { + id: 'messages.shared.returnToChatRooms', + defaultMessage: 'return to existing chat rooms', + }, + loadingMoreProfiles: { + id: 'messages.shared.loadingMoreProfiles', + defaultMessage: 'loading more profiles', + }, + seeProfile: { + id: 'messages.shared.seeProfile', + defaultMessage: 'See Profile', + }, + leaveGroup: { + id: 'messages.shared.leaveGroup', + defaultMessage: 'Leave Group', + }, + contactDetails: { + id: 'messages.shared.contactDetails', + defaultMessage: 'Contact Details', + }, + members: { + id: 'messages.shared.members', + defaultMessage: 'Members', + }, + remove: { + id: 'messages.shared.remove', + defaultMessage: 'Remove', + }, + addMember: { + id: 'messages.shared.addMember', + defaultMessage: 'Add Member', + }, +}) + +export const GROUP_FORM_VALIDATION_MESSAGES = defineMessages({ + titleRequired: { + id: 'messages.groupForm.validation.titleRequired', + defaultMessage: 'Please enter a title', + }, + titleMaxLength: { + id: 'messages.groupForm.validation.titleMaxLength', + defaultMessage: "Title can't be more than 20 characters", + }, + participantsRequired: { + id: 'messages.groupForm.validation.participantsRequired', + defaultMessage: 'Please select at least one member', + }, +}) + +export const getCreateOrEditGroupFormValidation = (intl: IntlShape) => + z.object({ + ...DEFAULT_CREATE_OR_EDIT_GROUP_FORM_VALIDATION.shape, + [CREATE_OR_EDIT_GROUP_FORM_VALUE.title]: z + .string() + .min(1, { message: intl.formatMessage(GROUP_FORM_VALIDATION_MESSAGES.titleRequired) }) + .max(20, { message: intl.formatMessage(GROUP_FORM_VALIDATION_MESSAGES.titleMaxLength) }), + [CREATE_OR_EDIT_GROUP_FORM_VALUE.participants]: z.array(z.any()).min(1, { + message: intl.formatMessage(GROUP_FORM_VALIDATION_MESSAGES.participantsRequired), + }), + }) diff --git a/packages/components/modules/messages/web/__shared__/utils.ts b/packages/components/modules/messages/web/__shared__/utils.ts new file mode 100644 index 000000000..a51a99917 --- /dev/null +++ b/packages/components/modules/messages/web/__shared__/utils.ts @@ -0,0 +1,11 @@ +import { IntlShape } from 'react-intl' + +import { SHARED_MESSAGES } from './constants' + +export const getParticipantCountLabel = ( + intl: IntlShape, + participantCount: number | null | undefined, +) => + participantCount === undefined || participantCount === null + ? undefined + : intl.formatMessage(SHARED_MESSAGES.memberCount, { count: participantCount }) diff --git a/packages/components/modules/navigations/web/Header/AccountMenu/AccountPopover/LogoutItem/index.tsx b/packages/components/modules/navigations/web/Header/AccountMenu/AccountPopover/LogoutItem/index.tsx index 76d313f46..da856af90 100644 --- a/packages/components/modules/navigations/web/Header/AccountMenu/AccountPopover/LogoutItem/index.tsx +++ b/packages/components/modules/navigations/web/Header/AccountMenu/AccountPopover/LogoutItem/index.tsx @@ -3,13 +3,12 @@ import { FC } from 'react' import { useLogout } from '@baseapp-frontend/authentication' import { ButtonBase, MenuItem } from '@mui/material' +import { useIntl } from 'react-intl' import { LogoutItemProps } from './types' -const LogoutItem: FC = ({ - handlePopoverOnClose, - logoutButtonLabel = 'Logout', -}) => { +const LogoutItem: FC = ({ handlePopoverOnClose, logoutButtonLabel }) => { + const intl = useIntl() const { logout } = useLogout() return ( @@ -23,7 +22,11 @@ const LogoutItem: FC = ({ }} sx={{ fontWeight: 'fontWeightBold', color: 'error.main' }} > - {logoutButtonLabel} + {logoutButtonLabel ?? + intl.formatMessage({ + id: 'navigations.accountPopover.logout', + defaultMessage: 'Logout', + })} ) } diff --git a/packages/components/modules/navigations/web/Header/AccountMenu/AccountPopover/index.tsx b/packages/components/modules/navigations/web/Header/AccountMenu/AccountPopover/index.tsx index ca3ba9af0..c5cd13ae8 100644 --- a/packages/components/modules/navigations/web/Header/AccountMenu/AccountPopover/index.tsx +++ b/packages/components/modules/navigations/web/Header/AccountMenu/AccountPopover/index.tsx @@ -7,6 +7,8 @@ import { ClickableAvatar } from '@baseapp-frontend/design-system/components/web/ import { Popover } from '@baseapp-frontend/design-system/components/web/popovers' import { usePopover } from '@baseapp-frontend/design-system/hooks/common' +import { useIntl } from 'react-intl' + // TODO: review importing components directly from another module import { AddProfileMenuItem as DefaultAddProfileMenuItem, @@ -34,6 +36,7 @@ const AccountPopover: FC = ({ AddProfileMenuItemProps = {}, LogoutItemProps = {}, }) => { + const intl = useIntl() const { currentProfile: profile } = useCurrentProfile() const popover = usePopover() @@ -66,7 +69,10 @@ const AccountPopover: FC = ({ diff --git a/packages/components/modules/navigations/web/Header/AccountMenu/VerticalFooter/index.tsx b/packages/components/modules/navigations/web/Header/AccountMenu/VerticalFooter/index.tsx index c5cf89987..e6a2a822e 100644 --- a/packages/components/modules/navigations/web/Header/AccountMenu/VerticalFooter/index.tsx +++ b/packages/components/modules/navigations/web/Header/AccountMenu/VerticalFooter/index.tsx @@ -11,6 +11,7 @@ import { useUISettings } from '@baseapp-frontend/design-system/hooks/web' import { joinWithSeparator } from '@baseapp-frontend/utils' import { Box, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { AddProfileMenuItem as DefaultAddProfileMenuItem, @@ -32,6 +33,7 @@ import { import { VerticalFooterProps } from './types' const VerticalFooter: FC = ({ AccountPopoverProps = {} }) => { + const intl = useIntl() const { currentProfile: profile } = useCurrentProfile() const { user } = useJWTUser() const popover = usePopover() @@ -99,7 +101,10 @@ const VerticalFooter: FC = ({ AccountPopoverProps = {} }) = width={AVATAR_SIZE} height={AVATAR_SIZE} src={profile?.image ?? ''} - alt="Profile avatar" + alt={intl.formatMessage({ + id: 'navigations.verticalFooter.avatarAlt', + defaultMessage: 'Profile avatar', + })} color="secondary" sx={{ border: (theme) => `2px solid ${theme.palette.divider}`, diff --git a/packages/components/modules/navigations/web/Header/AccountMenu/index.tsx b/packages/components/modules/navigations/web/Header/AccountMenu/index.tsx index 48d36caaf..21b0ddc45 100644 --- a/packages/components/modules/navigations/web/Header/AccountMenu/index.tsx +++ b/packages/components/modules/navigations/web/Header/AccountMenu/index.tsx @@ -8,6 +8,7 @@ import { JWTContent } from '@baseapp-frontend/utils' import { Button, Divider } from '@mui/material' import { Box } from '@mui/system' +import { useIntl } from 'react-intl' import AccountPopover from './AccountPopover' import VerticalFooter from './VerticalFooter' @@ -18,15 +19,22 @@ const AccountMenu: FC = ({ AccountPopoverProps = {}, additionalComponent, children, - loginButtonLabel = 'Login', + loginButtonLabel: loginButtonLabelProp, loginButtonProps, onLoginClick, onRegisterClick, - registerButtonLabel = 'Register', + registerButtonLabel: registerButtonLabelProp, registerButtonProps, vertical = false, }) => { + const intl = useIntl() const { user } = useJWTUser() + const loginButtonLabel = + loginButtonLabelProp ?? + intl.formatMessage({ id: 'navigations.accountMenu.login', defaultMessage: 'Login' }) + const registerButtonLabel = + registerButtonLabelProp ?? + intl.formatMessage({ id: 'navigations.accountMenu.register', defaultMessage: 'Register' }) const { settings } = useUISettings() const isNavMini = useMemo(() => settings.themeLayout === 'mini', [settings.themeLayout]) diff --git a/packages/components/modules/notifications/web/NotificationsList/EmptyState/index.tsx b/packages/components/modules/notifications/web/NotificationsList/EmptyState/index.tsx index cb50ed99d..1ec95733b 100644 --- a/packages/components/modules/notifications/web/NotificationsList/EmptyState/index.tsx +++ b/packages/components/modules/notifications/web/NotificationsList/EmptyState/index.tsx @@ -3,6 +3,7 @@ import { FC } from 'react' import { NotificationBellIcon as DefaultNotificationBellIcon } from '@baseapp-frontend/design-system/components/web/icons' import { Box, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' import { Container as DefaultContainer } from './styled' import { EmptyStateProps } from './types' @@ -18,9 +19,17 @@ const EmptyState: FC = ({ {...NotificationBellIconProps} /> - You don’t have notifications. + + + - Your future notifications will be shown here. + diff --git a/packages/components/modules/notifications/web/NotificationsList/LoadingState/index.tsx b/packages/components/modules/notifications/web/NotificationsList/LoadingState/index.tsx index 2cacd951e..e9328ce8d 100644 --- a/packages/components/modules/notifications/web/NotificationsList/LoadingState/index.tsx +++ b/packages/components/modules/notifications/web/NotificationsList/LoadingState/index.tsx @@ -1,7 +1,9 @@ import { LoadingState as BaseAppLoadingState } from '@baseapp-frontend/design-system/components/web/displays' import { Box, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' +import { NOTIFICATIONS_LIST_MESSAGES } from '../constants' import { HeaderContainer } from '../styled' const LoadingState = () => ( @@ -12,7 +14,9 @@ const LoadingState = () => ( sx={{ backgroundColor: 'common.white' }} > - Notifications + + + diff --git a/packages/components/modules/notifications/web/NotificationsList/MarkAllAsReadButton/index.tsx b/packages/components/modules/notifications/web/NotificationsList/MarkAllAsReadButton/index.tsx index 7cb256410..846676ced 100644 --- a/packages/components/modules/notifications/web/NotificationsList/MarkAllAsReadButton/index.tsx +++ b/packages/components/modules/notifications/web/NotificationsList/MarkAllAsReadButton/index.tsx @@ -1,6 +1,7 @@ import { FC } from 'react' import LoadingButton from '@mui/lab/LoadingButton' +import { FormattedMessage } from 'react-intl' import { useNotificationsMarkAllAsRead } from '../../../common' import { MarkAllAsReadButtonProps } from './types' @@ -20,7 +21,7 @@ const MarkAllAsReadButton: FC = ({ refetch }) => { return ( - Mark all as read + ) } diff --git a/packages/components/modules/notifications/web/NotificationsList/NotificationItem/CommentCreated/index.tsx b/packages/components/modules/notifications/web/NotificationsList/NotificationItem/CommentCreated/index.tsx index 0f31f5376..494c37809 100644 --- a/packages/components/modules/notifications/web/NotificationsList/NotificationItem/CommentCreated/index.tsx +++ b/packages/components/modules/notifications/web/NotificationsList/NotificationItem/CommentCreated/index.tsx @@ -2,6 +2,8 @@ import { FC } from 'react' import { formatRelativeTime } from '@baseapp-frontend/utils' +import { useIntl } from 'react-intl' + import DefaultNotificationAvatar from '../Notification/NotificationAvatar' import DefaultNotificationBody from '../Notification/NotificationBody' import DefaultNotificationHeader from '../Notification/NotificationHeader' @@ -22,8 +24,16 @@ const CommentCreated: FC = ({ NotificationBody = DefaultNotificationBody, NotificationBodyProps = {}, }) => { - // eslint-disable-next-line no-underscore-dangle - const message = `commented in your ${notification.target?.__typename?.toLowerCase?.() ?? ''}` + const intl = useIntl() + const message = intl.formatMessage( + { + id: 'notifications.commentCreated.message', + defaultMessage: + 'commented in {target, select, comment {your comment} contentpost {your contentpost} file {your file} page {your page} other {your {target}}}', + }, + // eslint-disable-next-line no-underscore-dangle + { target: notification.target?.__typename?.toLowerCase?.() ?? '' }, + ) return ( diff --git a/packages/components/modules/notifications/web/NotificationsList/NotificationItem/CommentReply/index.tsx b/packages/components/modules/notifications/web/NotificationsList/NotificationItem/CommentReply/index.tsx index 0d693723c..3497e5d25 100644 --- a/packages/components/modules/notifications/web/NotificationsList/NotificationItem/CommentReply/index.tsx +++ b/packages/components/modules/notifications/web/NotificationsList/NotificationItem/CommentReply/index.tsx @@ -2,6 +2,8 @@ import { FC } from 'react' import { formatRelativeTime } from '@baseapp-frontend/utils' +import { useIntl } from 'react-intl' + import DefaultNotificationAvatar from '../Notification/NotificationAvatar' import DefaultNotificationBody from '../Notification/NotificationBody' import DefaultNotificationHeader from '../Notification/NotificationHeader' @@ -22,8 +24,11 @@ const CommentReply: FC = ({ NotificationBody = DefaultNotificationBody, NotificationBodyProps = {}, }) => { - // eslint-disable-next-line no-underscore-dangle - const message = `replied to your comment` + const intl = useIntl() + const message = intl.formatMessage({ + id: 'notifications.commentReply.message', + defaultMessage: 'replied to your comment', + }) return ( diff --git a/packages/components/modules/notifications/web/NotificationsList/NotificationItem/Notification/NotificationAvatar/index.tsx b/packages/components/modules/notifications/web/NotificationsList/NotificationItem/Notification/NotificationAvatar/index.tsx index b37e6cd36..fc22cd1c3 100644 --- a/packages/components/modules/notifications/web/NotificationsList/NotificationItem/Notification/NotificationAvatar/index.tsx +++ b/packages/components/modules/notifications/web/NotificationsList/NotificationItem/Notification/NotificationAvatar/index.tsx @@ -2,15 +2,27 @@ import { FC } from 'react' import { AvatarWithPlaceholder } from '@baseapp-frontend/design-system/components/web/avatars' +import { useIntl } from 'react-intl' + import { NotificationAvatarProps } from './types' -const NotificationAvatar: FC = ({ actorAvatar, actorName }) => ( - -) +const NotificationAvatar: FC = ({ actorAvatar, actorName }) => { + const intl = useIntl() + + return ( + + ) +} export default NotificationAvatar diff --git a/packages/components/modules/notifications/web/NotificationsList/NotificationItem/ReactionCreated/index.tsx b/packages/components/modules/notifications/web/NotificationsList/NotificationItem/ReactionCreated/index.tsx index 45506d0e4..57eab4f16 100644 --- a/packages/components/modules/notifications/web/NotificationsList/NotificationItem/ReactionCreated/index.tsx +++ b/packages/components/modules/notifications/web/NotificationsList/NotificationItem/ReactionCreated/index.tsx @@ -2,6 +2,8 @@ import { FC } from 'react' import { formatRelativeTime } from '@baseapp-frontend/utils' +import { useIntl } from 'react-intl' + import DefaultNotificationAvatar from '../Notification/NotificationAvatar' import DefaultNotificationHeader from '../Notification/NotificationHeader' import { @@ -19,8 +21,16 @@ const ReactionCreated: FC = ({ NotificationHeader = DefaultNotificationHeader, NotificationHeaderProps = {}, }) => { - // eslint-disable-next-line no-underscore-dangle - const message = `liked your ${notification.target?.__typename?.toLowerCase?.() ?? ''}` + const intl = useIntl() + const message = intl.formatMessage( + { + id: 'notifications.reactionCreated.message', + defaultMessage: + 'liked {target, select, comment {your comment} contentpost {your contentpost} file {your file} page {your page} other {your {target}}}', + }, + // eslint-disable-next-line no-underscore-dangle + { target: notification.target?.__typename?.toLowerCase?.() ?? '' }, + ) return ( diff --git a/packages/components/modules/notifications/web/NotificationsList/constants.ts b/packages/components/modules/notifications/web/NotificationsList/constants.ts new file mode 100644 index 000000000..f5d52fc56 --- /dev/null +++ b/packages/components/modules/notifications/web/NotificationsList/constants.ts @@ -0,0 +1,8 @@ +import { defineMessages } from 'react-intl' + +export const NOTIFICATIONS_LIST_MESSAGES = defineMessages({ + title: { + id: 'notifications.list.title', + defaultMessage: 'Notifications', + }, +}) diff --git a/packages/components/modules/notifications/web/NotificationsList/index.tsx b/packages/components/modules/notifications/web/NotificationsList/index.tsx index ff736b266..b20c542aa 100644 --- a/packages/components/modules/notifications/web/NotificationsList/index.tsx +++ b/packages/components/modules/notifications/web/NotificationsList/index.tsx @@ -6,6 +6,7 @@ import { CloseIcon } from '@baseapp-frontend/design-system/components/web/icons' import { useResponsive } from '@baseapp-frontend/design-system/hooks/web' import { Box, Divider, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useLazyLoadQuery, usePaginationFragment } from 'react-relay' import { Virtuoso } from 'react-virtuoso' @@ -19,6 +20,7 @@ import { import DefaultEmptyState from './EmptyState' import MarkAllAsReadButton from './MarkAllAsReadButton' import DefaultNotificationItem from './NotificationItem' +import { NOTIFICATIONS_LIST_MESSAGES } from './constants' import { HeaderContainer as DefaultHeaderContainer, ListContainer as DefaultListContainer, @@ -37,6 +39,7 @@ const NotificationsList: FC = ({ HeaderContainer = DefaultHeaderContainer, ListContainer = DefaultListContainer, }) => { + const intl = useIntl() const smDown = useResponsive('down', 'sm') const options = { count: 10 } @@ -64,7 +67,9 @@ const NotificationsList: FC = ({ return ( <> - Older + + {intl.formatMessage({ id: 'notifications.list.older', defaultMessage: 'Older' })} + = ({ return ( - Notifications + + {intl.formatMessage(NOTIFICATIONS_LIST_MESSAGES.title)} + {!!data?.notificationsUnreadCount && ( )} {smDown && ( - setIsDrawerOpened(false)} aria-label="close notifications"> + setIsDrawerOpened(false)} + aria-label={intl.formatMessage({ + id: 'notifications.list.closeAriaLabel', + defaultMessage: 'close notifications', + })} + > )} diff --git a/packages/components/modules/notifications/web/NotificationsPopover/constants.ts b/packages/components/modules/notifications/web/NotificationsPopover/constants.ts new file mode 100644 index 000000000..5bc3026e1 --- /dev/null +++ b/packages/components/modules/notifications/web/NotificationsPopover/constants.ts @@ -0,0 +1,12 @@ +import { defineMessages } from 'react-intl' + +export const NOTIFICATIONS_POPOVER_MESSAGES = defineMessages({ + label: { + id: 'notifications.popover.label', + defaultMessage: 'Notifications', + }, + ariaLabel: { + id: 'notifications.popover.ariaLabel', + defaultMessage: 'see notifications', + }, +}) diff --git a/packages/components/modules/notifications/web/NotificationsPopover/index.tsx b/packages/components/modules/notifications/web/NotificationsPopover/index.tsx index 232628bd6..0ec6cf393 100644 --- a/packages/components/modules/notifications/web/NotificationsPopover/index.tsx +++ b/packages/components/modules/notifications/web/NotificationsPopover/index.tsx @@ -9,6 +9,7 @@ import { useResponsive } from '@baseapp-frontend/design-system/hooks/web' import { Badge as DefaultBadge, Drawer as DefaultDrawer, Typography } from '@mui/material' import IconButton from '@mui/material/IconButton' import { m } from 'framer-motion' +import { FormattedMessage, useIntl } from 'react-intl' import { useFragment, useLazyLoadQuery } from 'react-relay' import { NotificationUserMenuFragment$key } from '../../../../__generated__/NotificationUserMenuFragment.graphql' @@ -20,6 +21,7 @@ import { NotificationsPopoverQuery, } from '../../common' import DefaultNotificationsList from '../NotificationsList' +import { NOTIFICATIONS_POPOVER_MESSAGES } from './constants' import { NotificationsButton, NotificationsFallbackContainer } from './styled' import { NotificationsPopoverProps } from './types' @@ -37,6 +39,7 @@ const NotificationsPopover: FC = ({ currentLayout, labelComponent, }) => { + const intl = useIntl() const [isDrawerOpened, setIsDrawerOpened] = useState(false) const { me } = useLazyLoadQuery( @@ -78,7 +81,7 @@ const NotificationsPopover: FC = ({ } }} onClick={() => setIsDrawerOpened(true)} - aria-label="see notifications" + aria-label={intl.formatMessage(NOTIFICATIONS_POPOVER_MESSAGES.ariaLabel)} > @@ -90,7 +93,7 @@ const NotificationsPopover: FC = ({ fontWeight={currentLayout === 'mini' ? 600 : 500} fontSize={currentLayout === 'mini' ? 10 : 14} > - Notifications + )} @@ -101,7 +104,7 @@ const NotificationsPopover: FC = ({ whileHover="hover" variants={varHover(1.05)} onClick={() => setIsDrawerOpened(true)} - aria-label="see notifications" + aria-label={intl.formatMessage(NOTIFICATIONS_POPOVER_MESSAGES.ariaLabel)} > @@ -163,7 +166,7 @@ const NotificationsPopoverSuspended: FC = (props) => fontWeight={currentLayout === 'mini' ? 600 : 500} fontSize={currentLayout === 'mini' ? 10 : 14} > - Notifications + )) : null} diff --git a/packages/components/modules/pages/web/PageComponent/index.tsx b/packages/components/modules/pages/web/PageComponent/index.tsx index 05d4c4a9a..88bb94cdf 100644 --- a/packages/components/modules/pages/web/PageComponent/index.tsx +++ b/packages/components/modules/pages/web/PageComponent/index.tsx @@ -6,6 +6,7 @@ import { Markdown } from '@baseapp-frontend/design-system/components/web/markdow import { Box, Container, Link, Typography } from '@mui/material' import 'highlight.js/styles/base16/tomorrow-night.css' +import { FormattedMessage } from 'react-intl' import { useFragment } from 'react-relay' import rehypeHighlight from 'rehype-highlight' @@ -28,7 +29,7 @@ export const PageComponent = ({ page: pageRef }: PageComponentProps) => { target="_blank" rel="noopener noreferrer" > - Edit + )} {page.canDelete && ( @@ -38,7 +39,7 @@ export const PageComponent = ({ page: pageRef }: PageComponentProps) => { target="_blank" rel="noopener noreferrer" > - Delete + )} diff --git a/packages/components/modules/payments/web/AddCardModal/index.tsx b/packages/components/modules/payments/web/AddCardModal/index.tsx index cc9a0b898..b80c05755 100644 --- a/packages/components/modules/payments/web/AddCardModal/index.tsx +++ b/packages/components/modules/payments/web/AddCardModal/index.tsx @@ -8,7 +8,9 @@ import { useNotification } from '@baseapp-frontend/utils' import { Box, Button, Divider, LinearProgress, Typography } from '@mui/material' import { AddressElement, PaymentElement } from '@stripe/react-stripe-js' import { useQueryClient } from '@tanstack/react-query' +import { FormattedMessage, useIntl } from 'react-intl' +import { PAYMENTS_MESSAGES } from '../constants' import { STRIPE_API_KEY } from '../services/stripe' import { AddCardModalProps } from './types' @@ -22,18 +24,31 @@ const AddCardModal: FC = ({ }) => { const queryClient = useQueryClient() const { sendToast } = useNotification() + const intl = useIntl() const [isAddingCardPaymentProcessing, setIsAddingCardPaymentProcessing] = useState(false) const handleConfirmSetup = async () => { if (!stripe || !elements) return const addressElement = elements.getElement(AddressElement) if (!addressElement) { - sendToast('Address element is missing. Please try again.', { type: 'error' }) + sendToast( + intl.formatMessage({ + id: 'payments.addCard.addressMissing', + defaultMessage: 'Address element is missing. Please try again.', + }), + { type: 'error' }, + ) return } const addressValue = await addressElement.getValue() if (!addressValue.complete) { - sendToast('Error confirming card: Incomplete address', { type: 'error' }) + sendToast( + intl.formatMessage({ + id: 'payments.addCard.incompleteAddress', + defaultMessage: 'Error confirming card: Incomplete address', + }), + { type: 'error' }, + ) return } try { @@ -46,9 +61,23 @@ const AddCardModal: FC = ({ redirect: 'if_required', }) if (error) { - sendToast(`Error confirming card: ${error.message || 'Unknown error'}`, { - type: 'error', - }) + sendToast( + intl.formatMessage( + { + id: 'payments.addCard.confirmError', + defaultMessage: 'Error confirming card: {message}', + }, + { + message: + error.message || + intl.formatMessage({ + id: 'payments.addCard.unknownError', + defaultMessage: 'Unknown error', + }), + }, + ), + { type: 'error' }, + ) setIsAddingCardPaymentProcessing(false) } else { await queryClient.invalidateQueries({ @@ -58,13 +87,24 @@ const AddCardModal: FC = ({ if (handleSetupSuccess) { handleSetupSuccess(setupIntent?.payment_method as string) } - sendToast('Card added successfully') + sendToast( + intl.formatMessage({ + id: 'payments.addCard.success', + defaultMessage: 'Card added successfully', + }), + ) onClose() setIsAddingCardPaymentProcessing(false) } } catch (error: any) { console.error('Error confirming card:', error) - sendToast('Error confirming card:', { type: 'error' }) + sendToast( + intl.formatMessage({ + id: 'payments.addCard.genericError', + defaultMessage: 'Error confirming card:', + }), + { type: 'error' }, + ) setIsAddingCardPaymentProcessing(false) } } @@ -80,14 +120,26 @@ const AddCardModal: FC = ({ alignItems: 'center', }} > - Add payment method + + + - Card Information + + + {elements ? ( <> - Billing Address + + + = ({ }} > diff --git a/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/index.tsx b/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/index.tsx index bb3e5bd5c..15b063bea 100644 --- a/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/index.tsx +++ b/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/index.tsx @@ -3,7 +3,9 @@ import { CheckMarkIcon } from '@baseapp-frontend/design-system/components/web/ic import { Box, Button, CardContent, Chip, Typography } from '@mui/material' import Image from 'next/image' import Link from 'next/link' +import { FormattedMessage, useIntl } from 'react-intl' +import { PAYMENTS_MESSAGES } from '../../constants' import { SubscriptionCardWrapper } from '../styled' import { SubscriptionCardProps } from '../types' @@ -15,8 +17,19 @@ const SubscriptionCard = ({ manageHref, subscribeHref, }: SubscriptionCardProps) => { - const dolarPrice = Math.floor((sub.defaultPrice?.unitAmount ?? 0) / 100) - const centsPrice = ((sub.defaultPrice?.unitAmount ?? 0) % 100).toString().padStart(2, '0') + const intl = useIntl() + const priceParts = intl.formatNumberToParts((sub.defaultPrice?.unitAmount ?? 0) / 100, { + style: 'currency', + currency: 'USD', + }) + const joinParts = (types: string[]) => + priceParts + .filter((part) => types.includes(part.type)) + .map((part) => part.value) + .join('') + const currencySymbol = joinParts(['currency']) + const dolarPrice = joinParts(['integer', 'group']) + const centsPrice = joinParts(['decimal', 'fraction']) const marketingFeatures = sub.marketingFeatures ?? [] return ( @@ -27,26 +40,39 @@ const SubscriptionCard = ({ )} {sub.name} - {isActive && } + {isActive && ( + + )} - $ + {currencySymbol} {dolarPrice} - .{centsPrice} + {centsPrice} - USD/{selectedTerm} + {selectedTerm === 'yearly' ? ( + + ) : ( + + )} {sub.description} {isActive ? ( ) : ( )} diff --git a/packages/components/modules/payments/web/AvailableSubscriptions/index.tsx b/packages/components/modules/payments/web/AvailableSubscriptions/index.tsx index ec32e75a8..252678f8f 100644 --- a/packages/components/modules/payments/web/AvailableSubscriptions/index.tsx +++ b/packages/components/modules/payments/web/AvailableSubscriptions/index.tsx @@ -6,6 +6,7 @@ import { LoadingState } from '@baseapp-frontend/design-system/components/web/dis import { useResponsive } from '@baseapp-frontend/design-system/hooks/web' import { Box, ToggleButton, ToggleButtonGroup } from '@mui/material' +import { FormattedMessage } from 'react-intl' import { CHECKOUT_URL } from '../constants' import useStripeHook from '../hooks/useStripeHook' @@ -47,10 +48,14 @@ const AvailableSubscriptions: FC = ({ }} > {monthlySubs?.length && monthlySubs?.length > 0 && ( - Monthly + + + )} {yearlySubs?.length && yearlySubs?.length > 0 && ( - Yearly + + + )} diff --git a/packages/components/modules/payments/web/CheckoutComponent/ConfirmationSubscriptionModal/index.tsx b/packages/components/modules/payments/web/CheckoutComponent/ConfirmationSubscriptionModal/index.tsx index ca62d1480..7822ae624 100644 --- a/packages/components/modules/payments/web/CheckoutComponent/ConfirmationSubscriptionModal/index.tsx +++ b/packages/components/modules/payments/web/CheckoutComponent/ConfirmationSubscriptionModal/index.tsx @@ -7,6 +7,7 @@ import { ConfirmDialog } from '@baseapp-frontend/design-system/components/web/di import { Box, Button, Typography } from '@mui/material' import Link from 'next/link' +import { FormattedMessage, useIntl } from 'react-intl' import { maskEmail } from '../utils' import { ConfirmationSubscriptionModalProps } from './types' @@ -17,6 +18,7 @@ const ConfirmationSubscriptionModal: FC = ({ orderNumber, planDetailsHref, }) => { + const intl = useIntl() const user = getUser() const maskedEmail = user?.email ? maskEmail(user.email) : '' @@ -24,14 +26,25 @@ const ConfirmationSubscriptionModal: FC = ({ - Thank you for your subscription! Access the plan details by clicking on the button - below - {user?.email && ` or in the email we just sent to.`} + {user?.email ? ( + + ) : ( + + )} @@ -48,7 +61,10 @@ const ConfirmationSubscriptionModal: FC = ({ alignItems="center" > - Order Number: + {orderNumber} @@ -72,7 +88,7 @@ const ConfirmationSubscriptionModal: FC = ({ width: 'auto', }} > - Plan Details + } onClose={onClose} diff --git a/packages/components/modules/payments/web/CheckoutComponent/constants.ts b/packages/components/modules/payments/web/CheckoutComponent/constants.ts index e1e0b4a5a..c2cbc048f 100644 --- a/packages/components/modules/payments/web/CheckoutComponent/constants.ts +++ b/packages/components/modules/payments/web/CheckoutComponent/constants.ts @@ -1,3 +1,5 @@ +import { defineMessages } from 'react-intl' + /** * Rendered size of the product thumbnail, in px. * @@ -5,3 +7,22 @@ * correctly-sized WebP/AVIF and reserve the space before the image lands. */ export const PRODUCT_THUMBNAIL_SIZE = 48 + +export const CHECKOUT_MESSAGES = defineMessages({ + updateFailed: { + id: 'payments.checkout.updateFailed', + defaultMessage: 'Failed to update subscription', + }, + paymentConfirmationFailed: { + id: 'payments.checkout.paymentConfirmationFailed', + defaultMessage: 'Payment confirmation failed: {message}', + }, + createFailed: { + id: 'payments.checkout.createFailed', + defaultMessage: 'Failed to create subscription: {message}', + }, + unexpectedError: { + id: 'payments.checkout.unexpectedError', + defaultMessage: 'An unexpected error occurred. Please try again.', + }, +}) diff --git a/packages/components/modules/payments/web/CheckoutComponent/index.tsx b/packages/components/modules/payments/web/CheckoutComponent/index.tsx index e3530ca75..8407d228a 100644 --- a/packages/components/modules/payments/web/CheckoutComponent/index.tsx +++ b/packages/components/modules/payments/web/CheckoutComponent/index.tsx @@ -10,14 +10,16 @@ import { Box, Theme, useMediaQuery } from '@mui/system' import { AddressElement, Elements, useElements, useStripe } from '@stripe/react-stripe-js' import { useQueryClient } from '@tanstack/react-query' import Image from 'next/image' +import { FormattedMessage, useIntl } from 'react-intl' import PaymentDropdown from '../PaymentDropDown' +import { PAYMENTS_MESSAGES } from '../constants' import useStripeHook from '../hooks/useStripeHook' import { STRIPE_API_KEY } from '../services/stripe' import { formatPrice } from '../utils' import { getStripePromise } from '../utils/stripe' import DefaultConfirmationSubscriptionModal from './ConfirmationSubscriptionModal' -import { PRODUCT_THUMBNAIL_SIZE } from './constants' +import { CHECKOUT_MESSAGES, PRODUCT_THUMBNAIL_SIZE } from './constants' import { ProductContainer, StyledLoadingButton } from './styled' import { CheckoutComponentProps, CheckoutComponentWithElementProps } from './types' import { buildAddressOptions, extractErrorMessage } from './utils' @@ -39,6 +41,9 @@ const CheckoutComponent: FC = ({ const [selectedPaymentMethodId, setSelectedPaymentMethodId] = useState('') const { sendToast } = useNotification() + const intl = useIntl() + const getErrorMessage = (error: unknown) => + extractErrorMessage(error, intl.formatMessage(CHECKOUT_MESSAGES.unexpectedError)) const isMobile = useMediaQuery((theme) => theme.breakpoints.down('md')) const elements = useElements() const stripe = useStripe() @@ -69,13 +74,13 @@ const CheckoutComponent: FC = ({ useUpdateSubscription(customer?.subscriptions?.[0]?.id ?? '', { onSuccess: () => { setIsRetry(false) - sendToast('Subscription updated successfully.', { type: 'success' }) + sendToast(intl.formatMessage(PAYMENTS_MESSAGES.subscriptionUpdated), { type: 'success' }) setConfirmationModalOpen(true) onSuccess?.() }, onError: (error: any) => { console.error('Failed to update subscription', error) - sendToast('Failed to update subscription', { type: 'error' }) + sendToast(intl.formatMessage(CHECKOUT_MESSAGES.updateFailed), { type: 'error' }) setIsRetry(true) }, }) @@ -142,8 +147,11 @@ const CheckoutComponent: FC = ({ paymentMethodId: selectedPaymentMethodId, }) } catch (error) { - const message = extractErrorMessage(error) - sendToast(`Payment confirmation failed: ${message}`, { type: 'error' }) + const message = getErrorMessage(error) + sendToast( + intl.formatMessage(CHECKOUT_MESSAGES.paymentConfirmationFailed, { message }), + { type: 'error' }, + ) setPendingClientSecret(clientSecret) setIsRetry(true) return @@ -167,15 +175,20 @@ const CheckoutComponent: FC = ({ }, onError: (error: any) => { console.error('Failed to create subscription', error) - const message = extractErrorMessage(error) + const message = getErrorMessage(error) sendToast(message, { type: 'error' }) setIsRetry(true) }, }, ) } catch (error: any) { - const message = error?.response?.data?.error || error?.message || 'Please try again.' - sendToast(`Failed to create subscription: ${message}`, { type: 'error' }) + const message = + error?.response?.data?.error || + error?.message || + intl.formatMessage(PAYMENTS_MESSAGES.tryAgain) + sendToast(intl.formatMessage(CHECKOUT_MESSAGES.createFailed, { message }), { + type: 'error', + }) setIsRetry(true) } } @@ -197,8 +210,10 @@ const CheckoutComponent: FC = ({ setConfirmationModalOpen(true) onSuccess?.() } catch (error) { - const message = extractErrorMessage(error) - sendToast(`Payment confirmation failed: ${message}`, { type: 'error' }) + const message = getErrorMessage(error) + sendToast(intl.formatMessage(CHECKOUT_MESSAGES.paymentConfirmationFailed, { message }), { + type: 'error', + }) } } @@ -258,24 +273,26 @@ const CheckoutComponent: FC = ({ {formatPrice( product?.defaultPrice?.unitAmount, - // Price carries no locale, so this fell back to en-US anyway. - // Sourcing a real one from the user is a separate change. - undefined, + intl.locale, product?.defaultPrice?.currency, )} )} - + taxes /month + - By completing your purchase, you consent to BaseApp storing your payment method for - future charges. You can change your payment method at any time in your account - settings. + @@ -295,7 +312,14 @@ const CheckoutComponent: FC = ({ paymentMethods?.length === 0 } > - {isRetry ? 'Retry' : 'Place Order'} + {isRetry ? ( + + ) : ( + + )} @@ -303,7 +327,9 @@ const CheckoutComponent: FC = ({ - Payment + + + {elements && stripe && ( = ({ {shouldRenderAddressElement ? ( <> - Address + + + - Used to calculate taxes. + ({ * `nonFieldErrors` on the way in) and some viewsets as `{"error": "..."}`; packages that * have not been migrated yet still answer a bare list, hence the positional fallback. */ -export const extractErrorMessage = (error: any): string => +export const extractErrorMessage = ( + error: any, + fallbackMessage: string = 'An unexpected error occurred. Please try again.', +): string => error?.response?.data?.nonFieldErrors?.[0] || error?.response?.data?.error || (Array.isArray(error?.response?.data) && error?.response?.data[0]) || error?.message || - 'An unexpected error occurred. Please try again.' + fallbackMessage diff --git a/packages/components/modules/payments/web/InvoiceListTable/InvoiceItemTableRow/index.tsx b/packages/components/modules/payments/web/InvoiceListTable/InvoiceItemTableRow/index.tsx index b6005fdd2..71875ffb2 100644 --- a/packages/components/modules/payments/web/InvoiceListTable/InvoiceItemTableRow/index.tsx +++ b/packages/components/modules/payments/web/InvoiceListTable/InvoiceItemTableRow/index.tsx @@ -1,5 +1,7 @@ import { Button, Chip, TableCell, TableRow, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' +import { INVOICE_TABLE_MESSAGES } from '../constants' import { InvoiceItemTableRowProps } from '../types' const InvoiceItemTableRow = ({ @@ -8,6 +10,7 @@ const InvoiceItemTableRow = ({ cellProps, formattedDate, amountDue, + statusLabel, color, }: InvoiceItemTableRowProps) => ( @@ -18,9 +21,9 @@ const InvoiceItemTableRow = ({ {formattedDate} - + - {amountDue ? `$${amountDue}` : '-'} + {amountDue || '-'} diff --git a/packages/components/modules/payments/web/InvoiceListTable/InvoiceItemWrapper/index.tsx b/packages/components/modules/payments/web/InvoiceListTable/InvoiceItemWrapper/index.tsx index 8b90986cc..c38d9b498 100644 --- a/packages/components/modules/payments/web/InvoiceListTable/InvoiceItemWrapper/index.tsx +++ b/packages/components/modules/payments/web/InvoiceListTable/InvoiceItemWrapper/index.tsx @@ -1,14 +1,23 @@ -import { DATE_FORMAT, formatDateFromApi } from '@baseapp-frontend/utils' +import { useIntl } from 'react-intl' import { STATUS_COLORS } from '../../utils' import InvoiceItemTableRow from '../InvoiceItemTableRow' import MobileInvoiceItemTableRow from '../MobileInvoiceItemTableRow' +import { INVOICE_STATUS_MESSAGES } from '../constants' import { InvoiceItemWrapperProps } from '../types' const InvoiceItemWrapper = ({ row, rowProps, cellProps, smDown }: InvoiceItemWrapperProps) => { - const amountDue = (row.amountDue / 100).toFixed(2) ?? '' - const formattedDate = - formatDateFromApi(row.webhooksDeliveredAt, { toFormat: DATE_FORMAT[1] }) ?? '' + const intl = useIntl() + const amountDue = intl.formatNumber(row.amountDue / 100, { style: 'currency', currency: 'USD' }) + const formattedDate = row.webhooksDeliveredAt + ? intl.formatDate(row.webhooksDeliveredAt, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : '' + const statusMessage = INVOICE_STATUS_MESSAGES[row.status] + const statusLabel = statusMessage ? intl.formatMessage(statusMessage) : row.status const getStatusColor = (status: string) => STATUS_COLORS[status as keyof typeof STATUS_COLORS] @@ -20,6 +29,7 @@ const InvoiceItemWrapper = ({ row, rowProps, cellProps, smDown }: InvoiceItemWra cellProps={cellProps} formattedDate={formattedDate} amountDue={amountDue} + statusLabel={statusLabel} color={getStatusColor(row.status)} /> ) @@ -32,6 +42,7 @@ const InvoiceItemWrapper = ({ row, rowProps, cellProps, smDown }: InvoiceItemWra cellProps={cellProps} formattedDate={formattedDate} amountDue={amountDue} + statusLabel={statusLabel} color={getStatusColor(row.status)} /> ) diff --git a/packages/components/modules/payments/web/InvoiceListTable/InvoiceListTableHeader/index.tsx b/packages/components/modules/payments/web/InvoiceListTable/InvoiceListTableHeader/index.tsx index 0af167641..2ee47d150 100644 --- a/packages/components/modules/payments/web/InvoiceListTable/InvoiceListTableHeader/index.tsx +++ b/packages/components/modules/payments/web/InvoiceListTable/InvoiceListTableHeader/index.tsx @@ -1,5 +1,7 @@ import { TableCell, TableHead, TableRow } from '@mui/material' +import { FormattedMessage } from 'react-intl' +import { INVOICE_TABLE_MESSAGES } from '../constants' import { InvoiceListTableHeaderProps } from '../types' const InvoiceListTableHeader = ({ smDown, headerProps }: InvoiceListTableHeaderProps) => { @@ -7,8 +9,12 @@ const InvoiceListTableHeader = ({ smDown, headerProps }: InvoiceListTableHeaderP return ( - Description - Info + + + + + + @@ -18,11 +24,21 @@ const InvoiceListTableHeader = ({ smDown, headerProps }: InvoiceListTableHeaderP return ( - Description - Date - Status - Amount - Details + + + + + + + + + + + + + + + ) diff --git a/packages/components/modules/payments/web/InvoiceListTable/MobileInvoiceItemTableRow/index.tsx b/packages/components/modules/payments/web/InvoiceListTable/MobileInvoiceItemTableRow/index.tsx index 663f62a32..9be22554c 100644 --- a/packages/components/modules/payments/web/InvoiceListTable/MobileInvoiceItemTableRow/index.tsx +++ b/packages/components/modules/payments/web/InvoiceListTable/MobileInvoiceItemTableRow/index.tsx @@ -10,6 +10,7 @@ const MobileInvoiceItemTableRow = ({ cellProps, formattedDate, amountDue, + statusLabel, color, }: InvoiceItemTableRowProps) => ( @@ -32,8 +33,8 @@ const MobileInvoiceItemTableRow = ({ alignItems: 'flex-end', }} > - - {amountDue ? `$${amountDue}` : '-'} + + {amountDue || '-'} diff --git a/packages/components/modules/payments/web/InvoiceListTable/constants.ts b/packages/components/modules/payments/web/InvoiceListTable/constants.ts new file mode 100644 index 000000000..659b887e4 --- /dev/null +++ b/packages/components/modules/payments/web/InvoiceListTable/constants.ts @@ -0,0 +1,43 @@ +import { MessageDescriptor, defineMessages } from 'react-intl' + +export const INVOICE_STATUS_MESSAGES: Record = defineMessages({ + draft: { + id: 'payments.invoices.status.draft', + defaultMessage: 'draft', + }, + open: { + id: 'payments.invoices.status.open', + defaultMessage: 'open', + }, + paid: { + id: 'payments.invoices.status.paid', + defaultMessage: 'paid', + }, + uncollectible: { + id: 'payments.invoices.status.uncollectible', + defaultMessage: 'uncollectible', + }, + void: { + id: 'payments.invoices.status.void', + defaultMessage: 'void', + }, + failed: { + id: 'payments.invoices.status.failed', + defaultMessage: 'failed', + }, + pending: { + id: 'payments.invoices.status.pending', + defaultMessage: 'pending', + }, +}) + +export const INVOICE_TABLE_MESSAGES = defineMessages({ + description: { + id: 'payments.invoices.header.description', + defaultMessage: 'Description', + }, + receipt: { + id: 'payments.invoices.receipt', + defaultMessage: 'Receipt', + }, +}) diff --git a/packages/components/modules/payments/web/InvoiceListTable/index.tsx b/packages/components/modules/payments/web/InvoiceListTable/index.tsx index 79c1e6d80..b5428dc8e 100644 --- a/packages/components/modules/payments/web/InvoiceListTable/index.tsx +++ b/packages/components/modules/payments/web/InvoiceListTable/index.tsx @@ -14,6 +14,7 @@ import { TableRow, Typography, } from '@mui/material' +import { FormattedMessage, useIntl } from 'react-intl' import useStripeHook from '../hooks/useStripeHook' import { Invoice } from '../types' @@ -34,6 +35,7 @@ const InvoiceListTable = ({ entityId, }: InvoiceListTableProps) => { const [page, setPage] = useState(1) + const intl = useIntl() const smDown = useResponsive('down', 'sm') const { useListInvoices } = useStripeHook() @@ -48,7 +50,11 @@ const InvoiceListTable = ({ return ( - {title ?? 'Subscription Receipts'} + {title ?? + intl.formatMessage({ + id: 'payments.invoices.title', + defaultMessage: 'Subscription Receipts', + })} {isLoading ? ( @@ -68,7 +74,10 @@ const InvoiceListTable = ({ - No receipts found + diff --git a/packages/components/modules/payments/web/InvoiceListTable/types.ts b/packages/components/modules/payments/web/InvoiceListTable/types.ts index e93bb59b9..bed93a408 100644 --- a/packages/components/modules/payments/web/InvoiceListTable/types.ts +++ b/packages/components/modules/payments/web/InvoiceListTable/types.ts @@ -36,6 +36,7 @@ export interface InvoiceItemTableRowProps { cellProps?: TableCellProps formattedDate: string amountDue: string + statusLabel: string color: 'success' | 'error' | 'warning' } diff --git a/packages/components/modules/payments/web/PaymentDropDown/AddPaymentMethodItem/index.tsx b/packages/components/modules/payments/web/PaymentDropDown/AddPaymentMethodItem/index.tsx index 9dc34dc61..46663fcb5 100644 --- a/packages/components/modules/payments/web/PaymentDropDown/AddPaymentMethodItem/index.tsx +++ b/packages/components/modules/payments/web/PaymentDropDown/AddPaymentMethodItem/index.tsx @@ -1,6 +1,9 @@ import { AddIcon, CreditCardIcon } from '@baseapp-frontend/design-system/components/web/icons' import { Box, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' + +import { PAYMENTS_MESSAGES } from '../../constants' const AddPaymentMethodItem = ({ isSelected = false }: { isSelected?: boolean }) => ( - {isSelected ? 'Select the payment method' : 'Add payment method'} + {isSelected ? ( + + ) : ( + + )} {!isSelected && } diff --git a/packages/components/modules/payments/web/PaymentDropDown/PaymentMethodDisplay/index.tsx b/packages/components/modules/payments/web/PaymentDropDown/PaymentMethodDisplay/index.tsx index 2042dcb3c..59b9151b8 100644 --- a/packages/components/modules/payments/web/PaymentDropDown/PaymentMethodDisplay/index.tsx +++ b/packages/components/modules/payments/web/PaymentDropDown/PaymentMethodDisplay/index.tsx @@ -1,7 +1,9 @@ import { FC } from 'react' import { Box, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' +import { PAYMENTS_MESSAGES } from '../../constants' import { PaymentMethodDisplayProps } from './types' const PaymentMethodDisplay: FC = ({ @@ -13,14 +15,19 @@ const PaymentMethodDisplay: FC = ({ {getCardIcon(pm?.card?.brand)} - {pm?.card?.brand - ? pm.card.brand.charAt(0).toUpperCase() + pm.card.brand.slice(1).toLowerCase() - : 'Card'}{' '} + {pm?.card?.brand ? ( + pm.card.brand.charAt(0).toUpperCase() + pm.card.brand.slice(1).toLowerCase() + ) : ( + + )}{' '} ••• ••• ••• {pm?.card?.last4} {isSelected && ( - Expires: {pm?.card?.expMonth}/{pm?.card?.expYear} + )} diff --git a/packages/components/modules/payments/web/PaymentMethodsManagementComponent/PaymentMethodsItem/index.tsx b/packages/components/modules/payments/web/PaymentMethodsManagementComponent/PaymentMethodsItem/index.tsx index 125ec111e..a1442cc40 100644 --- a/packages/components/modules/payments/web/PaymentMethodsManagementComponent/PaymentMethodsItem/index.tsx +++ b/packages/components/modules/payments/web/PaymentMethodsManagementComponent/PaymentMethodsItem/index.tsx @@ -2,7 +2,9 @@ import { FC } from 'react' import { MoreVert } from '@mui/icons-material' import { Box, Chip, Divider, IconButton, Typography } from '@mui/material' +import { useIntl } from 'react-intl' +import { PAYMENTS_MESSAGES } from '../../constants' import { getCardIcon } from '../../utils' import { PaymentMethodsItemProps } from '../types' @@ -13,6 +15,7 @@ const PaymentMethodsItem: FC = ({ setAnchorEl, isLast, }) => { + const intl = useIntl() const cardExpiryDate = paymentMethod?.card?.expMonth && paymentMethod?.card?.expYear ? new Date(paymentMethod.card.expYear, paymentMethod.card.expMonth) @@ -25,15 +28,37 @@ const PaymentMethodsItem: FC = ({ {getCardIcon(paymentMethod?.card?.brand)} - {paymentMethod?.isDefault && } - {isExpired && } + {paymentMethod?.isDefault && ( + + )} + {isExpired && ( + + )} - {paymentMethod?.card?.brand?.toUpperCase() ?? 'CARD'} •••• •••• ••••{' '} - {paymentMethod?.card?.last4} + {paymentMethod?.card?.brand?.toUpperCase() ?? + intl.formatMessage({ + id: 'payments.paymentMethods.cardFallback', + defaultMessage: 'CARD', + })}{' '} + •••• •••• •••• {paymentMethod?.card?.last4} - {`${isExpired ? 'Expired' : 'Expires'}: ${paymentMethod?.card?.expMonth}/${paymentMethod?.card?.expYear}`} + {intl.formatMessage( + isExpired ? PAYMENTS_MESSAGES.cardExpired : PAYMENTS_MESSAGES.cardExpires, + { month: paymentMethod?.card?.expMonth, year: paymentMethod?.card?.expYear }, + )} (null) const { sendToast } = useNotification() + const intl = useIntl() const { useListPaymentMethods, useDeletePaymentMethod, useUpdatePaymentMethod, useSetupIntent } = useStripeHook() const elements = useElements() @@ -40,20 +43,54 @@ const PaymentMethodsManagementComponent: FC { - sendToast('Payment method removed successfully', { type: 'success' }) + sendToast( + intl.formatMessage({ + id: 'payments.paymentMethods.removeSuccess', + defaultMessage: 'Payment method removed successfully', + }), + { type: 'success' }, + ) }, onError: (error: any) => { - const message = getApiErrorMessage(error, { defaultMessage: 'Please try again.' }) - sendToast(`Failed to delete payment method: ${message}`, { type: 'error' }) + const message = getApiErrorMessage(error, { + defaultMessage: intl.formatMessage(PAYMENTS_MESSAGES.tryAgain), + }) + sendToast( + intl.formatMessage( + { + id: 'payments.paymentMethods.removeFailed', + defaultMessage: 'Failed to delete payment method: {message}', + }, + { message }, + ), + { type: 'error' }, + ) }, }) const { mutate: updatePaymentMethod } = useUpdatePaymentMethod({ onSuccess: () => { - sendToast('Default payment method set successfully', { type: 'success' }) + sendToast( + intl.formatMessage({ + id: 'payments.paymentMethods.setDefaultSuccess', + defaultMessage: 'Default payment method set successfully', + }), + { type: 'success' }, + ) }, onError: (error: any) => { - const message = getApiErrorMessage(error, { defaultMessage: 'Please try again.' }) - sendToast(`Failed to set default payment method: ${message}`, { type: 'error' }) + const message = getApiErrorMessage(error, { + defaultMessage: intl.formatMessage(PAYMENTS_MESSAGES.tryAgain), + }) + sendToast( + intl.formatMessage( + { + id: 'payments.paymentMethods.setDefaultFailed', + defaultMessage: 'Failed to set default payment method: {message}', + }, + { message }, + ), + { type: 'error' }, + ) }, }) const { @@ -107,10 +144,13 @@ const PaymentMethodsManagementComponent: FC - Payment Methods + - Add and manage your payment methods powered by Stripe. + {isLoadingMethods && } @@ -139,7 +179,7 @@ const PaymentMethodsManagementComponent: FC - Add payment method + - Set as default + + + { @@ -165,7 +210,7 @@ const PaymentMethodsManagementComponent: FC - Remove + @@ -186,11 +231,16 @@ const PaymentMethodsManagementComponent: FC setIsConfirmationDialogOpen(false)} aria-labelledby="delete-payment-method-confirmation-dialog" - title="Remove Payment Method" + title={intl.formatMessage({ + id: 'payments.paymentMethods.removeDialog.title', + defaultMessage: 'Remove Payment Method', + })} content={ - Are you sure you want to remove this payment method? If this is your only payment - method, your payments will be interrupted until a new method is added. + } action={ @@ -202,7 +252,14 @@ const PaymentMethodsManagementComponent: FC - {isDeletingPaymentMethod ? 'Removing...' : 'Remove'} + {isDeletingPaymentMethod ? ( + + ) : ( + + )} } /> diff --git a/packages/components/modules/payments/web/SubscriptionManagement/CancelSubscriptionModal/index.tsx b/packages/components/modules/payments/web/SubscriptionManagement/CancelSubscriptionModal/index.tsx index 538c55a43..a95262454 100644 --- a/packages/components/modules/payments/web/SubscriptionManagement/CancelSubscriptionModal/index.tsx +++ b/packages/components/modules/payments/web/SubscriptionManagement/CancelSubscriptionModal/index.tsx @@ -1,7 +1,9 @@ import { Dialog } from '@baseapp-frontend/design-system/components/web/dialogs' import { Box, Button, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' +import { PAYMENTS_MESSAGES } from '../../constants' import { CancelSubscriptionModalContainer } from '../styled' import { CancelSubscriptionModalProps } from '../types' @@ -14,21 +16,22 @@ const CancelSubscriptionModal: React.FC = ({ {isOpen && ( - Cancel Subscription + - Are you sure you want to cancel your subscription? You will retain access to your - current plan until the end of the billing period. After that, your account will be - downgraded to the free plan. + diff --git a/packages/components/modules/payments/web/SubscriptionManagement/FreePlanComponent/index.tsx b/packages/components/modules/payments/web/SubscriptionManagement/FreePlanComponent/index.tsx index f0d154389..d6a491f11 100644 --- a/packages/components/modules/payments/web/SubscriptionManagement/FreePlanComponent/index.tsx +++ b/packages/components/modules/payments/web/SubscriptionManagement/FreePlanComponent/index.tsx @@ -1,31 +1,34 @@ import { Check } from '@mui/icons-material' import { Box, Button, List, ListItem, ListItemIcon, ListItemText, Typography } from '@mui/material' import Link from 'next/link' +import { FormattedMessage, useIntl } from 'react-intl' +import { PAYMENTS_MESSAGES } from '../../constants' +import { FREE_PLAN_FEATURE_MESSAGES } from '../constants' import { RowFlexContainer, SubscriptionPlanContainer } from '../styled' import { FreePlanComponentProps } from './types' const FreePlanComponent = ({ planChangeUrl }: FreePlanComponentProps) => { - const freeFeatures = [ - 'Access to core features', - 'Limited storage space', - 'Standard support', - 'No commitment required', - ] + const intl = useIntl() + const freeFeatures = Object.values(FREE_PLAN_FEATURE_MESSAGES).map((message) => + intl.formatMessage(message), + ) return ( - Subscription + - Free + - Enjoy essential access to the platform at no cost. Perfect for exploring basic features - and getting started. + @@ -52,7 +55,7 @@ const FreePlanComponent = ({ planChangeUrl }: FreePlanComponentProps) => { width: 'fit-content', }} > - Change Plan + ) diff --git a/packages/components/modules/payments/web/SubscriptionManagement/constants.ts b/packages/components/modules/payments/web/SubscriptionManagement/constants.ts new file mode 100644 index 000000000..747da37c1 --- /dev/null +++ b/packages/components/modules/payments/web/SubscriptionManagement/constants.ts @@ -0,0 +1,43 @@ +import { MessageDescriptor, defineMessages } from 'react-intl' + +export const SUBSCRIPTION_STATUS_MESSAGES: Record = defineMessages({ + Active: { + id: 'payments.status.active', + defaultMessage: 'Active', + }, + Pending: { + id: 'payments.subscription.status.pending', + defaultMessage: 'Pending', + }, + Canceled: { + id: 'payments.subscription.status.canceled', + defaultMessage: 'Canceled', + }, + 'Past Due': { + id: 'payments.subscription.status.pastDue', + defaultMessage: 'Past Due', + }, + Unpaid: { + id: 'payments.subscription.status.unpaid', + defaultMessage: 'Unpaid', + }, +}) + +export const FREE_PLAN_FEATURE_MESSAGES = defineMessages({ + coreFeatures: { + id: 'payments.freePlan.features.coreFeatures', + defaultMessage: 'Access to core features', + }, + limitedStorage: { + id: 'payments.freePlan.features.limitedStorage', + defaultMessage: 'Limited storage space', + }, + standardSupport: { + id: 'payments.freePlan.features.standardSupport', + defaultMessage: 'Standard support', + }, + noCommitment: { + id: 'payments.freePlan.features.noCommitment', + defaultMessage: 'No commitment required', + }, +}) diff --git a/packages/components/modules/payments/web/SubscriptionManagement/index.tsx b/packages/components/modules/payments/web/SubscriptionManagement/index.tsx index fb7dfdebf..27e500094 100644 --- a/packages/components/modules/payments/web/SubscriptionManagement/index.tsx +++ b/packages/components/modules/payments/web/SubscriptionManagement/index.tsx @@ -1,8 +1,8 @@ 'use client' -import { FC, useEffect, useMemo, useState } from 'react' +import { FC, ReactNode, useEffect, useMemo, useState } from 'react' -import { formatDateFromApi, useNotification } from '@baseapp-frontend/utils' +import { useNotification } from '@baseapp-frontend/utils' import { Check } from '@mui/icons-material' import { @@ -23,14 +23,16 @@ import { Elements, useElements, useStripe } from '@stripe/react-stripe-js' import { useQueryClient } from '@tanstack/react-query' import Link from 'next/link' import { useSearchParams } from 'next/navigation' +import { FormattedMessage, useIntl } from 'react-intl' import PaymentDropdown from '../PaymentDropDown' -import { SUBSCRIPTIONS_URL } from '../constants' +import { PAYMENTS_MESSAGES, SUBSCRIPTIONS_URL } from '../constants' import useStripeHook from '../hooks/useStripeHook' import { STRIPE_API_KEY } from '../services/stripe' import { getStripePromise } from '../utils/stripe' import CancelSubscriptionModal from './CancelSubscriptionModal' import FreePlanComponent from './FreePlanComponent' +import { SUBSCRIPTION_STATUS_MESSAGES } from './constants' import { ColumnFlexContainer, PaymentMethodContainer, @@ -40,6 +42,8 @@ import { import { SubscriptionManagementProps } from './types' import { getChipLabelAndColorByStatus } from './utils' +const renderBold = (chunks: ReactNode[]) => {chunks} + const SubscriptionManagement: FC = ({ entityId }) => { const [lastAddedPaymentMethodIdDuringSession, setLastAddedPaymentMethodIdDuringSession] = useState(null) @@ -77,6 +81,7 @@ const SubscriptionManagement: FC = ({ entityId }) = invalidateAfterCancel, ) const { sendToast } = useNotification() + const intl = useIntl() const elements = useElements() const stripe = useStripe() const searchParams = useSearchParams() @@ -90,16 +95,25 @@ const SubscriptionManagement: FC = ({ entityId }) = queryClient.invalidateQueries({ queryKey: [STRIPE_API_KEY.getSubscription(subscriptionId ?? '')], }) - sendToast('Subscription updated successfully.', { type: 'success' }) + sendToast(intl.formatMessage(PAYMENTS_MESSAGES.subscriptionUpdated), { type: 'success' }) }, onError: (error) => { console.error('Error updating subscription:', error) - sendToast(`Failed to update payment method`, { type: 'error' }) + sendToast( + intl.formatMessage({ + id: 'payments.subscription.updatePaymentMethodFailed', + defaultMessage: 'Failed to update payment method', + }), + { type: 'error' }, + ) }, }) const amountDue = (subscription?.upcomingInvoice?.amountDue ?? 0) / 100 - const nextPaymentAttempt = formatDateFromApi(subscription?.upcomingInvoice?.nextPaymentAttempt) + const nextPaymentAttemptDate = subscription?.upcomingInvoice?.nextPaymentAttempt + const nextPaymentAttempt = nextPaymentAttemptDate + ? intl.formatDate(nextPaymentAttemptDate, { year: '2-digit', month: '2-digit', day: '2-digit' }) + : '' const hasNextBill = subscription?.status === 'active' const hasSubscription = customer?.subscriptions?.length && customer.subscriptions.length > 0 const marketingFeatures = subscription?.product?.marketingFeatures ?? [] @@ -116,6 +130,8 @@ const SubscriptionManagement: FC = ({ entityId }) = return fallbackDefault ? fallbackDefault.id : paymentMethods[0]?.id }, [paymentMethods, subscription?.defaultPaymentMethod, lastAddedPaymentMethodIdDuringSession]) const { label, color } = getChipLabelAndColorByStatus(subscription?.status ?? '') + const statusMessage = SUBSCRIPTION_STATUS_MESSAGES[label] + const statusLabel = statusMessage ? intl.formatMessage(statusMessage) : label const handleSetupSuccess = (paymentMethodId: string) => { if (paymentMethodId) { @@ -158,7 +174,7 @@ const SubscriptionManagement: FC = ({ entityId }) = return ( - Subscription + {isLoading && } {!isLoading && ( @@ -168,9 +184,13 @@ const SubscriptionManagement: FC = ({ entityId }) = - {subscription?.product?.name ?? 'Subscription Plan'} + {subscription?.product?.name ?? + intl.formatMessage({ + id: 'payments.subscription.planFallback', + defaultMessage: 'Subscription Plan', + })} - {label && color && } + {label && color && } {subscription?.product?.description ?? ''} @@ -201,18 +221,34 @@ const SubscriptionManagement: FC = ({ entityId }) = - Payment + + + {nextPaymentAttempt && hasNextBill && ( - Your next bill is{' '} {amountDue !== 0 ? ( - <> - for ${amountDue.toFixed(2)} - + ) : ( - 'free' - )}{' '} - on {nextPaymentAttempt} + + )} )} @@ -250,7 +286,7 @@ const SubscriptionManagement: FC = ({ entityId }) = width: 'fit-content', }} > - Cancel Subscription + { const { sendToast } = useNotification() + const intl = useIntl() const queryClient = useQueryClient() const useGetCustomer = (entityId?: string, options: { enabled?: boolean } = {}) => @@ -115,12 +117,27 @@ const useStripeHook = () => { payment_method: paymentMethodId, }) if (result.error) { - throw new Error(result.error.message || 'Failed to confirm card payment.') + throw new Error( + result.error.message || + intl.formatMessage({ + id: 'payments.stripe.confirmCardPaymentFailedFallback', + defaultMessage: 'Failed to confirm card payment.', + }), + ) } return result.paymentIntent }, onError: (error) => { - sendToast(`Failed to confirm card payment: ${error.message}`, { type: 'error' }) + sendToast( + intl.formatMessage( + { + id: 'payments.stripe.confirmCardPaymentFailed', + defaultMessage: 'Failed to confirm card payment: {message}', + }, + { message: error.message }, + ), + { type: 'error' }, + ) }, mutationKey: [STRIPE_API_KEY.confirmCardPayment()], }) @@ -181,11 +198,23 @@ const useStripeHook = () => { useMutation({ mutationFn: () => StripeApi.cancelSubscription(subscriptionId), onSuccess: () => { - sendToast('Subscription cancelled successfully.', { type: 'success' }) + sendToast( + intl.formatMessage({ + id: 'payments.stripe.cancelSubscriptionSuccess', + defaultMessage: 'Subscription cancelled successfully.', + }), + { type: 'success' }, + ) refetch() }, onError: () => { - sendToast(`Failed to cancel subscription`, { type: 'error' }) + sendToast( + intl.formatMessage({ + id: 'payments.stripe.cancelSubscriptionFailed', + defaultMessage: 'Failed to cancel subscription', + }), + { type: 'error' }, + ) }, mutationKey: [STRIPE_API_KEY.cancelSubscription(subscriptionId)], }) diff --git a/packages/components/modules/profiles/common/graphql/mutations/CancelInvitation.ts b/packages/components/modules/profiles/common/graphql/mutations/CancelInvitation.ts index 60ca00c59..176ccf7d7 100644 --- a/packages/components/modules/profiles/common/graphql/mutations/CancelInvitation.ts +++ b/packages/components/modules/profiles/common/graphql/mutations/CancelInvitation.ts @@ -1,5 +1,6 @@ import { useNotification } from '@baseapp-frontend/utils' +import { useIntl } from 'react-intl' import { Disposable, UseMutationConfig, graphql, useMutation } from 'react-relay' import { CancelInvitationMutation } from '../../../../../__generated__/CancelInvitationMutation.graphql' @@ -21,6 +22,7 @@ export const useCancelInvitationMutation = (): [ boolean, ] => { const { sendMutationErrorToast, sendToast } = useNotification() + const intl = useIntl() const [commitMutation, isMutationInFlight] = useMutation( CancelInvitationMutationQuery, ) @@ -29,19 +31,29 @@ export const useCancelInvitationMutation = (): [ commitMutation({ ...config, onCompleted: (response, errors) => { + const removeFailedMessage = intl.formatMessage({ + id: 'profiles.members.cancelInvitation.failed', + defaultMessage: 'Invitation could not be removed', + }) const errorMessage = sendMutationErrorToast( response?.profileCancelInvitation?.errors, errors, { - defaultMessage: 'Invitation could not be removed', + defaultMessage: removeFailedMessage, }, ) if (!errorMessage) { if (response?.profileCancelInvitation?.success === false) { - sendToast('Invitation could not be removed', { type: 'error' }) + sendToast(removeFailedMessage, { type: 'error' }) } else { - sendToast('Invitation removed', { type: 'success' }) + sendToast( + intl.formatMessage({ + id: 'profiles.members.cancelInvitation.success', + defaultMessage: 'Invitation removed', + }), + { type: 'success' }, + ) } } config?.onCompleted?.(response, errors) diff --git a/packages/components/modules/profiles/common/graphql/mutations/ProfileUserRoleCreate.ts b/packages/components/modules/profiles/common/graphql/mutations/ProfileUserRoleCreate.ts index 39a4f9536..909a377c4 100644 --- a/packages/components/modules/profiles/common/graphql/mutations/ProfileUserRoleCreate.ts +++ b/packages/components/modules/profiles/common/graphql/mutations/ProfileUserRoleCreate.ts @@ -1,6 +1,7 @@ import { getGraphQLErrorMessage } from '@baseapp-frontend/graphql' import { useNotification } from '@baseapp-frontend/utils' +import { useIntl } from 'react-intl' import { Disposable, UseMutationConfig, graphql, useMutation } from 'react-relay' import { ProfileUserRoleCreateMutation } from '../../../../../__generated__/ProfileUserRoleCreateMutation.graphql' @@ -31,6 +32,7 @@ export const useProfileUserRoleCreateMutation = (): [ boolean, ] => { const { sendToast } = useNotification() + const intl = useIntl() const [commitMutation, isMutationInFlight] = useMutation( ProfileUserRoleCreateMutationQuery, ) @@ -45,7 +47,16 @@ export const useProfileUserRoleCreateMutation = (): [ config.onError(error) return } - sendToast(getGraphQLErrorMessage(error, 'Failed to add members'), { type: 'error' }) + sendToast( + getGraphQLErrorMessage( + error, + intl.formatMessage({ + id: 'profiles.inviteMemberDialog.addMembersFailed', + defaultMessage: 'Failed to add members', + }), + ), + { type: 'error' }, + ) }, }) diff --git a/packages/components/modules/profiles/common/graphql/mutations/RemoveMember.ts b/packages/components/modules/profiles/common/graphql/mutations/RemoveMember.ts index bfa92eff9..04ff7cc35 100644 --- a/packages/components/modules/profiles/common/graphql/mutations/RemoveMember.ts +++ b/packages/components/modules/profiles/common/graphql/mutations/RemoveMember.ts @@ -1,5 +1,6 @@ import { useNotification } from '@baseapp-frontend/utils' +import { useIntl } from 'react-intl' import { Disposable, UseMutationConfig, graphql, useMutation } from 'react-relay' import { RemoveMemberMutation } from '../../../../../__generated__/RemoveMemberMutation.graphql' @@ -17,6 +18,7 @@ export const useRemoveMemberMutation = (): [ boolean, ] => { const { sendMutationErrorToast, sendToast } = useNotification() + const intl = useIntl() const [commitMutation, isMutationInFlight] = useMutation( ProfileRemoveMemberMutationQuery, ) @@ -27,7 +29,13 @@ export const useRemoveMemberMutation = (): [ onCompleted: (response, errors) => { const errorMessage = sendMutationErrorToast(undefined, errors) if (!errorMessage) { - sendToast('Member removed successfully', { type: 'success' }) + sendToast( + intl.formatMessage({ + id: 'profiles.members.removeMember.success', + defaultMessage: 'Member removed successfully', + }), + { type: 'success' }, + ) } config?.onCompleted?.(response, errors) }, diff --git a/packages/components/modules/profiles/common/graphql/mutations/ReportCreate.ts b/packages/components/modules/profiles/common/graphql/mutations/ReportCreate.ts index 5ddee5f7e..bf71b55d2 100644 --- a/packages/components/modules/profiles/common/graphql/mutations/ReportCreate.ts +++ b/packages/components/modules/profiles/common/graphql/mutations/ReportCreate.ts @@ -1,5 +1,6 @@ import { useNotification } from '@baseapp-frontend/utils' +import { useIntl } from 'react-intl' import { UseMutationConfig, graphql, useMutation } from 'react-relay' import { ReportCreateMutation } from '../../../../../__generated__/ReportCreateMutation.graphql' @@ -25,6 +26,7 @@ export const useReportCreateMutation = (): [ useMutation(ReportCreateMutationQuery) const { sendMutationErrorToast, sendToast } = useNotification() + const intl = useIntl() const commit = (config: UseMutationConfig) => { commitMutation({ ...config, @@ -34,7 +36,13 @@ export const useReportCreateMutation = (): [ }, onError: (error) => { if (error.message.includes('duplicate key value violates unique constraint')) { - sendToast('You have already reported this profile.', { type: 'error' }) + sendToast( + intl.formatMessage({ + id: 'profiles.report.alreadyReported', + defaultMessage: 'You have already reported this profile.', + }), + { type: 'error' }, + ) } else { sendToast(error.message, { type: 'error' }) } diff --git a/packages/components/modules/profiles/common/graphql/mutations/ResendInvitation.ts b/packages/components/modules/profiles/common/graphql/mutations/ResendInvitation.ts index 6fbf737cf..c07ed7117 100644 --- a/packages/components/modules/profiles/common/graphql/mutations/ResendInvitation.ts +++ b/packages/components/modules/profiles/common/graphql/mutations/ResendInvitation.ts @@ -1,5 +1,6 @@ import { useNotification } from '@baseapp-frontend/utils' +import { useIntl } from 'react-intl' import { Disposable, UseMutationConfig, graphql, useMutation } from 'react-relay' import { ResendInvitationMutation } from '../../../../../__generated__/ResendInvitationMutation.graphql' @@ -26,6 +27,7 @@ export const useResendInvitationMutation = (): [ boolean, ] => { const { sendMutationErrorToast, sendToast } = useNotification() + const intl = useIntl() const [commitMutation, isMutationInFlight] = useMutation( ResendInvitationMutationQuery, ) @@ -41,9 +43,21 @@ export const useResendInvitationMutation = (): [ if (!errorMessage) { if (response?.profileResendInvitation?.emailSent === false) { - sendToast('Invitation updated, but the email could not be sent', { type: 'warning' }) + sendToast( + intl.formatMessage({ + id: 'profiles.members.resendInvitation.emailNotSent', + defaultMessage: 'Invitation updated, but the email could not be sent', + }), + { type: 'warning' }, + ) } else { - sendToast('Invitation resent successfully', { type: 'success' }) + sendToast( + intl.formatMessage({ + id: 'profiles.members.resendInvitation.success', + defaultMessage: 'Invitation resent successfully', + }), + { type: 'success' }, + ) } } config?.onCompleted?.(response, errors) diff --git a/packages/components/modules/profiles/common/graphql/mutations/SendInvitation.ts b/packages/components/modules/profiles/common/graphql/mutations/SendInvitation.ts index 54a647ef2..acbca1126 100644 --- a/packages/components/modules/profiles/common/graphql/mutations/SendInvitation.ts +++ b/packages/components/modules/profiles/common/graphql/mutations/SendInvitation.ts @@ -1,6 +1,7 @@ import { getGraphQLErrorMessage } from '@baseapp-frontend/graphql' import { useNotification } from '@baseapp-frontend/utils' +import { useIntl } from 'react-intl' import { Disposable, UseMutationConfig, graphql, useMutation } from 'react-relay' import { SendInvitationMutation } from '../../../../../__generated__/SendInvitationMutation.graphql' @@ -31,6 +32,7 @@ export const useSendInvitationMutation = (): [ boolean, ] => { const { sendToast } = useNotification() + const intl = useIntl() const [commitMutation, isMutationInFlight] = useMutation( SendInvitationMutationQuery, ) @@ -45,7 +47,16 @@ export const useSendInvitationMutation = (): [ config.onError(error) return } - sendToast(getGraphQLErrorMessage(error, 'Failed to send invitations'), { type: 'error' }) + sendToast( + getGraphQLErrorMessage( + error, + intl.formatMessage({ + id: 'profiles.inviteMemberDialog.sendInvitationsFailed', + defaultMessage: 'Failed to send invitations', + }), + ), + { type: 'error' }, + ) }, }) diff --git a/packages/components/modules/profiles/web/PermissionDenied/index.tsx b/packages/components/modules/profiles/web/PermissionDenied/index.tsx index 1fe16a6be..748e51e3e 100644 --- a/packages/components/modules/profiles/web/PermissionDenied/index.tsx +++ b/packages/components/modules/profiles/web/PermissionDenied/index.tsx @@ -4,6 +4,7 @@ import { FC } from 'react' import { Alert, Button, Typography } from '@mui/material' import { useRouter } from 'next/navigation' +import { FormattedMessage } from 'react-intl' import { Container } from './styled' @@ -14,15 +15,20 @@ const PermissionDenied: FC = () => { - Permission Required + - You don't have permission to access this page. Please contact an administrator if you - believe this is an error. + ) diff --git a/packages/components/modules/profiles/web/ProfileComponent/BlockButtonWithDialog/constants.ts b/packages/components/modules/profiles/web/ProfileComponent/BlockButtonWithDialog/constants.ts index e88a5d73a..88b03c299 100644 --- a/packages/components/modules/profiles/web/ProfileComponent/BlockButtonWithDialog/constants.ts +++ b/packages/components/modules/profiles/web/ProfileComponent/BlockButtonWithDialog/constants.ts @@ -1,14 +1,46 @@ -export const BLOCK_UNBLOCK_DIALOG_TEXTS = { - block: { - title: 'Block', - content: +import { defineMessages } from 'react-intl' + +export const BLOCK_UNBLOCK_DIALOG_MESSAGES = defineMessages({ + blockTitle: { + id: 'profiles.blockDialog.block.title', + defaultMessage: 'Block {name}?', + }, + blockContent: { + id: 'profiles.blockDialog.block.content', + defaultMessage: "This person won’t be able to send you messages, find your profile or content. They won’t see your comments and reactions, and won't be able to mention or follow your profile. We won't notify them that they have been blocked.", - action: 'Block', }, - unblock: { - title: 'Unblock', - content: + blockAction: { + id: 'profiles.blockDialog.block.action', + defaultMessage: 'Block', + }, + unblockTitle: { + id: 'profiles.blockDialog.unblock.title', + defaultMessage: 'Unblock {name}?', + }, + unblockContent: { + id: 'profiles.blockDialog.unblock.content', + defaultMessage: "This person will be able to send you messages, find your profile or content. They will see your comments and reactions, and will be able to mention or follow your profile. We won't notify them that they have been unblocked.", - action: 'Unblock', }, -} + unblockAction: { + id: 'profiles.blockDialog.unblock.action', + defaultMessage: 'Unblock', + }, + blockProfile: { + id: 'profiles.blockDialog.blockProfile', + defaultMessage: 'Block profile', + }, + unblockProfile: { + id: 'profiles.blockDialog.unblockProfile', + defaultMessage: 'Unblock profile', + }, + blockedToast: { + id: 'profiles.blockDialog.blockedToast', + defaultMessage: '{name} is blocked', + }, + unblockedToast: { + id: 'profiles.blockDialog.unblockedToast', + defaultMessage: '{name} is unblocked', + }, +}) diff --git a/packages/components/modules/profiles/web/ProfileComponent/BlockButtonWithDialog/index.tsx b/packages/components/modules/profiles/web/ProfileComponent/BlockButtonWithDialog/index.tsx index 159dabeff..5536831ae 100644 --- a/packages/components/modules/profiles/web/ProfileComponent/BlockButtonWithDialog/index.tsx +++ b/packages/components/modules/profiles/web/ProfileComponent/BlockButtonWithDialog/index.tsx @@ -5,11 +5,12 @@ import { BlockIcon, UnblockIcon } from '@baseapp-frontend/design-system/componen import { useNotification } from '@baseapp-frontend/utils' import { Button, CircularProgress, MenuItem, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useFragment, useMutation } from 'react-relay' import { BlockToggleMutation } from '../../../../../__generated__/BlockToggleMutation.graphql' import { BlockToggleFragment, BlockToggleMutationQuery } from '../../../common' -import { BLOCK_UNBLOCK_DIALOG_TEXTS } from './constants' +import { BLOCK_UNBLOCK_DIALOG_MESSAGES as MESSAGES } from './constants' import { ActionButton, DialogTitleContainer } from './styled' import { BlockButtonWithDialogProps } from './types' @@ -25,6 +26,7 @@ const BlockButtonWithDialog: FC = ({ useMutation(BlockToggleMutationQuery) const { sendMutationErrorToast, sendToast } = useNotification() const [open, setOpen] = useState(false) + const intl = useIntl() const isBlockedByMe = target?.isBlockedByMe @@ -59,7 +61,12 @@ const BlockButtonWithDialog: FC = ({ } handleSuccess() sendToast( - `${target.name ?? ''} is ${response?.blockToggle?.target?.isBlockedByMe ? 'blocked' : 'unblocked'}`, + intl.formatMessage( + response?.blockToggle?.target?.isBlockedByMe + ? MESSAGES.blockedToast + : MESSAGES.unblockedToast, + { name: target.name ?? '' }, + ), { type: 'info' }, ) }, @@ -77,12 +84,12 @@ const BlockButtonWithDialog: FC = ({ {isBlockedByMe ? ( <> - Unblock profile + {intl.formatMessage(MESSAGES.unblockProfile)} ) : ( <> - Block profile + {intl.formatMessage(MESSAGES.blockProfile)} )} @@ -98,12 +105,12 @@ const BlockButtonWithDialog: FC = ({ {isBlockedByMe ? ( <> - Unblock + {intl.formatMessage(MESSAGES.unblockAction)} ) : ( <> - Block profile + {intl.formatMessage(MESSAGES.blockProfile)} )} @@ -114,14 +121,14 @@ const BlockButtonWithDialog: FC = ({ title={ {isBlockedByMe ? : } - {`${isBlockedByMe ? BLOCK_UNBLOCK_DIALOG_TEXTS.unblock.title : BLOCK_UNBLOCK_DIALOG_TEXTS.block.title} ${target.name}?`} + {intl.formatMessage(isBlockedByMe ? MESSAGES.unblockTitle : MESSAGES.blockTitle, { + name: target.name, + })} } - content={ - isBlockedByMe - ? BLOCK_UNBLOCK_DIALOG_TEXTS.unblock.content - : BLOCK_UNBLOCK_DIALOG_TEXTS.block.content - } + content={intl.formatMessage( + isBlockedByMe ? MESSAGES.unblockContent : MESSAGES.blockContent, + )} onClose={handleClose} action={ = ({ isBlocked={isBlockedByMe} disabled={isMutationInFlight} > - {isBlockedByMe - ? BLOCK_UNBLOCK_DIALOG_TEXTS.unblock.action - : BLOCK_UNBLOCK_DIALOG_TEXTS.block.action} + {intl.formatMessage(isBlockedByMe ? MESSAGES.unblockAction : MESSAGES.blockAction)} {isMutationInFlight && } } diff --git a/packages/components/modules/profiles/web/ProfileComponent/FollowToggleButton/index.tsx b/packages/components/modules/profiles/web/ProfileComponent/FollowToggleButton/index.tsx index 02b05cb8f..3a042de43 100644 --- a/packages/components/modules/profiles/web/ProfileComponent/FollowToggleButton/index.tsx +++ b/packages/components/modules/profiles/web/ProfileComponent/FollowToggleButton/index.tsx @@ -3,6 +3,7 @@ import { FC } from 'react' import { Iconify } from '@baseapp-frontend/design-system/components/web/images' import { Button } from '@mui/material' +import { FormattedMessage } from 'react-intl' import { useFollowToggle } from '../../../common' import { FollowToggleButtonProps } from './types' @@ -21,7 +22,11 @@ const FollowToggleButton: FC = ({ targetId, isFollowedB disabled={isMutationInFlight} size="medium" > - {isFollowedByMe ? 'Following' : 'Follow'} + {isFollowedByMe ? ( + + ) : ( + + )} ) } diff --git a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/ConfirmationStep.tsx b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/ConfirmationStep.tsx index ea6b71b1f..596fb0782 100644 --- a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/ConfirmationStep.tsx +++ b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/ConfirmationStep.tsx @@ -3,7 +3,9 @@ import { FC } from 'react' import { useResponsive } from '@baseapp-frontend/design-system/hooks/web' import { Button, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' +import { REPORT_MESSAGES } from '../constants' import { ConfirmationStepProps } from '../types' const ConfirmationStep: FC = ({ reportType, onClose }) => { @@ -12,13 +14,18 @@ const ConfirmationStep: FC = ({ reportType, onClose }) => return ( <> - Thanks for reporting {reportType?.label} + - Your report is anonymous. If someone is in immediate danger, call the local emergency - services - don't wait. + - + ) } diff --git a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/ReportDialogContent.tsx b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/ReportDialogContent.tsx index f404df53d..5176a2124 100644 --- a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/ReportDialogContent.tsx +++ b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/ReportDialogContent.tsx @@ -9,12 +9,13 @@ import { import { useResponsive } from '@baseapp-frontend/design-system/hooks/web' import { Box, CircularProgress, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' import { useLazyLoadQuery } from 'react-relay' import { ReportTypeListQuery as ReportTypeListQueryType } from '../../../../../../__generated__/ReportTypeListQuery.graphql' import { useReportCreateMutation } from '../../../../common/graphql/mutations/ReportCreate' import { ReportTypeListQuery } from '../../../../common/graphql/queries/ReportTypeList' -import { STEPS } from '../constants' +import { REPORT_MESSAGES, STEPS } from '../constants' import { ReportButtonWithDialogProps, ReportTypeNode, ReportTypeSubTypeNode } from '../types' import ConfirmationStep from './ConfirmationStep' import SelectReportTypeStep from './SelectReportTypeStep' @@ -150,13 +151,17 @@ const ReportDialogContent: FC = ({ targetId, handle ) : ( <> - Report + + + )} ) : ( - Report + + + )} {steps?.find((step) => step.name === currentStep)?.content} diff --git a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/SelectReportTypeStep.tsx b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/SelectReportTypeStep.tsx index fc75eef5b..032bec4a1 100644 --- a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/SelectReportTypeStep.tsx +++ b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/SelectReportTypeStep.tsx @@ -4,8 +4,9 @@ import { ChevronIcon } from '@baseapp-frontend/design-system/components/web/icon import { useResponsive } from '@baseapp-frontend/design-system/hooks/web' import { Box, Divider, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' -import { STEPS } from '../constants' +import { REPORT_MESSAGES, STEPS } from '../constants' import { TypeButton } from '../styled' import { ReportTypeNode, ReportTypeSubTypeNode, SelectReportTypeStepProps } from '../types' @@ -54,13 +55,12 @@ const SelectReportTypeStep: FC = ({ <> - Why are you reporting this? + - Your report is anonymous. If someone is in immediate danger, call the local emergency - services - don't wait. + diff --git a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/SummaryStep.tsx b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/SummaryStep.tsx index 4c57e3f7c..06fe6fd24 100644 --- a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/SummaryStep.tsx +++ b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/SummaryStep.tsx @@ -3,7 +3,9 @@ import { FC } from 'react' import { useResponsive } from '@baseapp-frontend/design-system/hooks/web' import { Box, Button, Divider, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' +import { REPORT_MESSAGES } from '../constants' import { SummaryStepProps } from '../types' const SummaryStep: FC = ({ @@ -18,35 +20,50 @@ const SummaryStep: FC = ({ return ( - You're about to submit a report + - Your report is anonymous. If someone is in immediate danger, call the local emergency - services - don't wait. + - Why are you reporting this? + + + {reportType?.label} {reportSubType && ( - What type of {reportType?.label}? + + + {reportSubType?.label} )} - About the problem + + + {reportText} ) diff --git a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/TextStep.tsx b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/TextStep.tsx index f75cb3159..a15d9f9a0 100644 --- a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/TextStep.tsx +++ b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/components/TextStep.tsx @@ -3,20 +3,28 @@ import { FC } from 'react' import { useResponsive } from '@baseapp-frontend/design-system/hooks/web' import { Button, Divider, TextField, Typography } from '@mui/material' +import { FormattedMessage, useIntl } from 'react-intl' import { STEPS } from '../constants' import { TextStepProps } from '../types' const TextStep: FC = ({ reportText, setReportText, setCurrentStep }) => { const smDown = useResponsive('down', 'sm') + const intl = useIntl() return ( <> - How would you describe the problem? + - Use the text field below to explain what the problem you are reporting is. + = ({ reportText, setReportText, setCurrentStep rows={4} value={reportText} onChange={(e) => setReportText(e.target.value)} - placeholder="I find the post to be offensive..." + placeholder={intl.formatMessage({ + id: 'profiles.report.text.placeholder', + defaultMessage: 'I find the post to be offensive...', + })} /> ) diff --git a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/constants.ts b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/constants.ts index 39b304bae..58a0fb60d 100644 --- a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/constants.ts +++ b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/constants.ts @@ -1,3 +1,5 @@ +import { defineMessages } from 'react-intl' + export const STEPS = { report: 'report', subTypes: 'subTypes', @@ -5,3 +7,19 @@ export const STEPS = { summary: 'summary', confirmation: 'confirmation', } + +export const REPORT_MESSAGES = defineMessages({ + title: { + id: 'profiles.report.title', + defaultMessage: 'Report', + }, + anonymousNotice: { + id: 'profiles.report.anonymousNotice', + defaultMessage: + "Your report is anonymous. If someone is in immediate danger, call the local emergency services - don't wait.", + }, + whyReporting: { + id: 'profiles.report.whyReporting', + defaultMessage: 'Why are you reporting this?', + }, +}) diff --git a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/index.tsx b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/index.tsx index a50e1ec20..16b2e7860 100644 --- a/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/index.tsx +++ b/packages/components/modules/profiles/web/ProfileComponent/ReportButtonWithDialog/index.tsx @@ -7,6 +7,7 @@ import { FlagIcon } from '@baseapp-frontend/design-system/components/web/icons' import { useResponsive } from '@baseapp-frontend/design-system/hooks/web' import { MenuItem, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' import ReportDialogContent from './components/ReportDialogContent' import { StyledSwipeableDrawer } from './styled' @@ -21,7 +22,7 @@ const ReportButtonWithDialog: FC = ({ targetId, han setIsReportModalOpen(true)} disableRipple> - Report profile + {smDown ? ( diff --git a/packages/components/modules/profiles/web/ProfileComponent/index.tsx b/packages/components/modules/profiles/web/ProfileComponent/index.tsx index 90d06c74a..85316f129 100644 --- a/packages/components/modules/profiles/web/ProfileComponent/index.tsx +++ b/packages/components/modules/profiles/web/ProfileComponent/index.tsx @@ -14,6 +14,7 @@ import { useNotification } from '@baseapp-frontend/utils' import { Button, Divider, MenuItem, Typography } from '@mui/material' import { useRouter } from 'next/navigation' +import { FormattedMessage, useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { ProfileComponentFragment, formatFollowCount } from '../../common' @@ -55,12 +56,19 @@ const ProfileComponent: FC = ({ setAnchorEl(null) } const { sendToast } = useNotification() + const intl = useIntl() const handleShareClick = () => { const path = profile?.urlPath?.path ?? `/profile/${profile?.id}` const url = [process.env.NEXT_PUBLIC_APP_BASE_URL, path].join('') navigator.clipboard.writeText(url) - sendToast('Profile URL copied to clipboard!', { type: 'success' }) + sendToast( + intl.formatMessage({ + id: 'profiles.profile.shareCopied', + defaultMessage: 'Profile URL copied to clipboard!', + }), + { type: 'success' }, + ) handleClose() } @@ -75,7 +83,7 @@ const ProfileComponent: FC = ({ sx={{ maxWidth: smDown ? '100%' : 'fit-content' }} onClick={() => router.push('/user/settings')} > - Edit Profile + ) } @@ -98,7 +106,7 @@ const ProfileComponent: FC = ({ const menuOptions = [ - Share profile + , smDown && , profile && currentProfileId !== profile?.id && ( @@ -123,7 +131,10 @@ const ProfileComponent: FC = ({ = ({ {formatFollowCount(profile?.followersCount)} - Followers + @@ -162,7 +173,7 @@ const ProfileComponent: FC = ({ {formatFollowCount(profile?.followingCount)} - Following + diff --git a/packages/components/modules/profiles/web/ProfileMembers/InviteMemberDialog/MemberSearch/index.tsx b/packages/components/modules/profiles/web/ProfileMembers/InviteMemberDialog/MemberSearch/index.tsx index e873487de..d81cb80bf 100644 --- a/packages/components/modules/profiles/web/ProfileMembers/InviteMemberDialog/MemberSearch/index.tsx +++ b/packages/components/modules/profiles/web/ProfileMembers/InviteMemberDialog/MemberSearch/index.tsx @@ -9,13 +9,14 @@ import { AutocompleteField } from '@baseapp-frontend/design-system/components/we import { useDebouncedValue } from '@baseapp-frontend/utils' import { Box, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useLazyLoadQuery } from 'react-relay' import { InviteMembersSearchQuery as InviteMembersSearchQueryType } from '../../../../../../__generated__/InviteMembersSearchQuery.graphql' import { InviteMembersSearchQuery, useInviteMembersSearch } from '../../../../common' import { - INVITE_MEMBER_DIALOG_COPY as COPY, EMAIL_REGEX, + INVITE_MEMBER_DIALOG_MESSAGES as MESSAGES, SEARCH_DEBOUNCE_MS, SEARCH_RESULTS_COUNT, } from '../constants' @@ -24,6 +25,7 @@ import { getMemberKey } from '../utils' import { ChipsList, MemberChip, OptionInfo } from './styled' const MemberSearch: FC = ({ selected, onAdd, onRemove }) => { + const intl = useIntl() const [query, setQuery] = useState('') const debouncedQuery = useDebouncedValue(query, { debounceTime: SEARCH_DEBOUNCE_MS }) const [isPending, startTransition] = useTransition() @@ -106,7 +108,7 @@ const MemberSearch: FC = ({ selected, onAdd, onRemove }) => { value={null} inputValue={query} isPending={isPending} - placeholder={COPY.searchPlaceholder} + placeholder={intl.formatMessage(MESSAGES.searchPlaceholder)} freeSolo={false} autoComplete // Results are filtered server-side via refetch; keep MUI from re-filtering them. @@ -190,7 +192,7 @@ const MemberSearch: FC = ({ selected, onAdd, onRemove }) => { )} onRemove(getMemberKey(member))} > diff --git a/packages/components/modules/profiles/web/ProfileMembers/InviteMemberDialog/constants.ts b/packages/components/modules/profiles/web/ProfileMembers/InviteMemberDialog/constants.ts index 85e6bf809..08c64d00c 100644 --- a/packages/components/modules/profiles/web/ProfileMembers/InviteMemberDialog/constants.ts +++ b/packages/components/modules/profiles/web/ProfileMembers/InviteMemberDialog/constants.ts @@ -1,10 +1,43 @@ -export const INVITE_MEMBER_DIALOG_COPY = { - title: 'Add member', - description: 'Add profiles to your organization or send an invitation email.', - searchPlaceholder: 'Invite members by name or email', - cancel: 'Cancel', - submit: 'Invite', -} as const +import { defineMessages } from 'react-intl' + +export const INVITE_MEMBER_DIALOG_MESSAGES = defineMessages({ + title: { + id: 'profiles.inviteMemberDialog.title', + defaultMessage: 'Add member', + }, + description: { + id: 'profiles.inviteMemberDialog.description', + defaultMessage: 'Add profiles to your organization or send an invitation email.', + }, + searchPlaceholder: { + id: 'profiles.inviteMemberDialog.searchPlaceholder', + defaultMessage: 'Invite members by name or email', + }, + cancel: { + id: 'common.cancel', + defaultMessage: 'Cancel', + }, + submit: { + id: 'profiles.inviteMemberDialog.submit', + defaultMessage: 'Invite', + }, + addMembersFailed: { + id: 'profiles.inviteMemberDialog.addMembersFailed', + defaultMessage: 'Failed to add members', + }, + sendInvitationsFailed: { + id: 'profiles.inviteMemberDialog.sendInvitationsFailed', + defaultMessage: 'Failed to send invitations', + }, + membersAdded: { + id: 'profiles.inviteMemberDialog.membersAdded', + defaultMessage: '{count, plural, one {Member added} other {# members added}}', + }, + removeMember: { + id: 'profiles.inviteMemberDialog.removeMember', + defaultMessage: 'Remove member', + }, +}) /** * Role assigned to newly added members / invitations. The design has no role picker in diff --git a/packages/components/modules/profiles/web/ProfileMembers/InviteMemberDialog/index.tsx b/packages/components/modules/profiles/web/ProfileMembers/InviteMemberDialog/index.tsx index e1001aab5..cb13c3f9c 100644 --- a/packages/components/modules/profiles/web/ProfileMembers/InviteMemberDialog/index.tsx +++ b/packages/components/modules/profiles/web/ProfileMembers/InviteMemberDialog/index.tsx @@ -9,11 +9,12 @@ import { getMutationErrorMessage, useNotification } from '@baseapp-frontend/util import { LoadingButton } from '@mui/lab' import { Box, TextField, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useProfileUserRoleCreateMutation } from '../../../common/graphql/mutations/ProfileUserRoleCreate' import { useSendInvitationMutation } from '../../../common/graphql/mutations/SendInvitation' import MemberSearch from './MemberSearch' -import { INVITE_MEMBER_DIALOG_COPY as COPY, DEFAULT_INVITE_ROLE } from './constants' +import { DEFAULT_INVITE_ROLE, INVITE_MEMBER_DIALOG_MESSAGES as MESSAGES } from './constants' import { InviteMemberDialogProps, SelectedEmail, SelectedMember, SelectedProfile } from './types' import { BatchError, @@ -27,6 +28,7 @@ import { const InviteMemberDialog: FC = ({ open, onClose, connections }) => { const { currentProfile } = useCurrentProfile() const { sendToast } = useNotification() + const intl = useIntl() const [selected, setSelected] = useState([]) const [isSubmitting, setIsSubmitting] = useState(false) const [createMembers] = useProfileUserRoleCreateMutation() @@ -62,13 +64,18 @@ const InviteMemberDialog: FC = ({ open, onClose, connec variables: { input: { profileId, usersIds, roleType: DEFAULT_INVITE_ROLE }, connections }, onCompleted: (response, errors) => { const message = getMutationErrorMessage(response?.profileUserRoleCreate?.errors, errors, { - defaultMessage: 'Failed to add members', + defaultMessage: intl.formatMessage(MESSAGES.addMembersFailed), }) if (message) reject(new BatchError(message, members)) else resolve(members) }, onError: (error) => - reject(new BatchError(getGraphQLErrorMessage(error, 'Failed to add members'), members)), + reject( + new BatchError( + getGraphQLErrorMessage(error, intl.formatMessage(MESSAGES.addMembersFailed)), + members, + ), + ), }) }) @@ -87,14 +94,17 @@ const InviteMemberDialog: FC = ({ open, onClose, connec }, onCompleted: (response, errors) => { const message = getMutationErrorMessage(response?.profileSendInvitation?.errors, errors, { - defaultMessage: 'Failed to send invitations', + defaultMessage: intl.formatMessage(MESSAGES.sendInvitationsFailed), }) if (message) reject(new BatchError(message, members)) else resolve(members) }, onError: (error) => reject( - new BatchError(getGraphQLErrorMessage(error, 'Failed to send invitations'), members), + new BatchError( + getGraphQLErrorMessage(error, intl.formatMessage(MESSAGES.sendInvitationsFailed)), + members, + ), ), }) }) @@ -122,7 +132,7 @@ const InviteMemberDialog: FC = ({ open, onClose, connec failed.forEach((error) => sendToast(error.message, { type: 'error' })) if (succeeded.length > 0) { - sendToast(succeeded.length === 1 ? 'Member added' : `${succeeded.length} members added`, { + sendToast(intl.formatMessage(MESSAGES.membersAdded, { count: succeeded.length }), { type: 'success', }) } @@ -141,20 +151,25 @@ const InviteMemberDialog: FC = ({ open, onClose, connec return ( - {COPY.description} + {intl.formatMessage(MESSAGES.description)} {open && ( + } > @@ -170,7 +185,7 @@ const InviteMemberDialog: FC = ({ open, onClose, connec loading={isSubmitting} disabled={isSubmitting || selected.length === 0} > - {COPY.submit} + {intl.formatMessage(MESSAGES.submit)} } /> diff --git a/packages/components/modules/profiles/web/ProfileMembers/MemberItem/index.tsx b/packages/components/modules/profiles/web/ProfileMembers/MemberItem/index.tsx index 15d22e35a..767c60771 100644 --- a/packages/components/modules/profiles/web/ProfileMembers/MemberItem/index.tsx +++ b/packages/components/modules/profiles/web/ProfileMembers/MemberItem/index.tsx @@ -5,6 +5,7 @@ import { AvatarWithPlaceholder } from '@baseapp-frontend/design-system/component import { ConfirmDialog } from '@baseapp-frontend/design-system/components/web/dialogs' import { Box, Button, MenuItem, SelectChangeEvent, Typography, useTheme } from '@mui/material' +import { FormattedMessage, useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { ProfileRoles } from '../../../../../__generated__/ChangeUserRoleMutation.graphql' @@ -16,7 +17,9 @@ import { useResendInvitationMutation } from '../../../common/graphql/mutations/R import { INVITATION_ACTIONS, MEMBER_ACTIONS, + MEMBER_MESSAGES, MEMBER_ROLES, + MEMBER_ROLE_AND_STATUS_MESSAGES, MEMBER_STATUSES, invitationActionOptions, roleOptions, @@ -40,6 +43,11 @@ const MemberItem: FC = ({ invitationId, }) => { const theme = useTheme() + const intl = useIntl() + const backText = intl.formatMessage({ + id: 'profiles.members.dialogs.back', + defaultMessage: 'Back', + }) const memberProfile = useFragment(ProfileItemFragment, member) @@ -176,7 +184,7 @@ const MemberItem: FC = ({ value="" onChange={handleInvitationAction} displayEmpty - renderValue={() => 'Expired'} + renderValue={() => intl.formatMessage(MEMBER_MESSAGES.expired)} variant="filled" size="small" disabled={isResendingInvitation || isCancellingInvitation} @@ -190,7 +198,7 @@ const MemberItem: FC = ({ value === INVITATION_ACTIONS.remove ? theme.palette.error.main : 'inherit', }} > - {label} + {intl.formatMessage(label)} ))} @@ -203,7 +211,7 @@ const MemberItem: FC = ({ tree as an actionable control (vs. pointerEvents:none, which only blocks the mouse). Greyed-out also reads correctly for an expired invitation. */} ) @@ -234,7 +242,7 @@ const MemberItem: FC = ({ color: value === MEMBER_ACTIONS.remove ? theme.palette.error.main : 'inherit', }} > - {label} + {intl.formatMessage(label)} ))} @@ -242,12 +250,14 @@ const MemberItem: FC = ({ ) } if (haveMemberRoleAndStatus) { + const roleOrStatus = status === MEMBER_STATUSES.active ? memberRole : status + const roleOrStatusMessage = MEMBER_ROLE_AND_STATUS_MESSAGES[roleOrStatus] return ( ) @@ -262,52 +272,68 @@ const MemberItem: FC = ({ return ( <> - Confirm + } onClose={cancelChangeRole} content={ - Are you sure you want to promote this member to an admin? They will have full - administrative rights, including the ability to manage members and settings. + } - cancelText="Back" + cancelText={backText} /> - Are you sure you want to remove this member? This action will revoke their access to the - organization profile. + } - cancelText="Back" + cancelText={backText} action={ } /> - Are you sure you want to remove this invitation? It will be deleted from the list. + } - cancelText="Back" + cancelText={backText} action={ } /> @@ -317,7 +343,10 @@ const MemberItem: FC = ({ width={avatarWidth} height={avatarHeight} src={memberProfile?.image ?? ''} - alt="Profile avatar" + alt={intl.formatMessage({ + id: 'profiles.members.avatarAlt', + defaultMessage: 'Profile avatar', + })} color="secondary" {...avatarProps} /> diff --git a/packages/components/modules/profiles/web/ProfileMembers/MembersList/index.tsx b/packages/components/modules/profiles/web/ProfileMembers/MembersList/index.tsx index 183666616..91fca2700 100644 --- a/packages/components/modules/profiles/web/ProfileMembers/MembersList/index.tsx +++ b/packages/components/modules/profiles/web/ProfileMembers/MembersList/index.tsx @@ -5,6 +5,7 @@ import { Searchbar } from '@baseapp-frontend/design-system/components/web/inputs import { Button, Typography } from '@mui/material' import { useForm } from 'react-hook-form' +import { FormattedMessage } from 'react-intl' import { ConnectionHandler, useFragment, usePaginationFragment } from 'react-relay' import { ProfileItemFragment$key } from '../../../../../__generated__/ProfileItemFragment.graphql' @@ -102,7 +103,11 @@ const MembersList: FC = ({ /> - {resultsCount === 1 ? `${resultsCount} member` : `${resultsCount} members`} + {data?.canAddMember && ( )} diff --git a/packages/components/modules/profiles/web/ProfileMembers/constants.ts b/packages/components/modules/profiles/web/ProfileMembers/constants.ts index 35067cc5b..298daecb4 100644 --- a/packages/components/modules/profiles/web/ProfileMembers/constants.ts +++ b/packages/components/modules/profiles/web/ProfileMembers/constants.ts @@ -1,4 +1,4 @@ -import { capitalizeFirstLetter } from './utils' +import { MessageDescriptor, defineMessages } from 'react-intl' export const NUMBER_OF_MEMBERS_TO_LOAD_NEXT = 5 export const NUMBER_OF_MEMBERS_ON_FIRST_LOAD = 10 @@ -16,9 +16,52 @@ export const INVITATION_ACTIONS = { remove: 'REMOVE', } as const +export const MEMBER_MESSAGES = defineMessages({ + resendInvitation: { + id: 'profiles.members.actions.resendInvitation', + defaultMessage: 'Resend Invitation', + }, + remove: { + id: 'profiles.members.actions.remove', + defaultMessage: 'Remove', + }, + admin: { + id: 'profiles.members.roles.admin', + defaultMessage: 'Admin', + }, + manager: { + id: 'profiles.members.roles.manager', + defaultMessage: 'Manager', + }, + owner: { + id: 'profiles.members.roles.owner', + defaultMessage: 'Owner', + }, + active: { + id: 'profiles.members.statuses.active', + defaultMessage: 'Active', + }, + pending: { + id: 'profiles.members.statuses.pending', + defaultMessage: 'Pending', + }, + inactive: { + id: 'profiles.members.statuses.inactive', + defaultMessage: 'Inactive', + }, + declined: { + id: 'profiles.members.statuses.declined', + defaultMessage: 'Declined', + }, + expired: { + id: 'profiles.members.statuses.expired', + defaultMessage: 'Expired', + }, +}) + export const invitationActionOptions = [ - { value: INVITATION_ACTIONS.resend, label: 'Resend Invitation' }, - { value: INVITATION_ACTIONS.remove, label: 'Remove' }, + { value: INVITATION_ACTIONS.resend, label: MEMBER_MESSAGES.resendInvitation }, + { value: INVITATION_ACTIONS.remove, label: MEMBER_MESSAGES.remove }, ] export const MEMBER_ROLES = { @@ -32,16 +75,18 @@ export const MEMBER_ACTIONS = { } export const roleOptions = [ - { - value: MEMBER_ROLES.admin, - label: capitalizeFirstLetter(MEMBER_ROLES.admin.toLowerCase()), - }, - { - value: MEMBER_ROLES.manager, - label: capitalizeFirstLetter(MEMBER_ROLES.manager.toLowerCase()), - }, - { - value: MEMBER_ACTIONS.remove, - label: capitalizeFirstLetter(MEMBER_ACTIONS.remove.toLowerCase()), - }, + { value: MEMBER_ROLES.admin, label: MEMBER_MESSAGES.admin }, + { value: MEMBER_ROLES.manager, label: MEMBER_MESSAGES.manager }, + { value: MEMBER_ACTIONS.remove, label: MEMBER_MESSAGES.remove }, ] + +export const MEMBER_ROLE_AND_STATUS_MESSAGES: Record = { + [MEMBER_ROLES.admin]: MEMBER_MESSAGES.admin, + [MEMBER_ROLES.manager]: MEMBER_MESSAGES.manager, + [MEMBER_ROLES.owner]: MEMBER_MESSAGES.owner, + [MEMBER_STATUSES.active]: MEMBER_MESSAGES.active, + [MEMBER_STATUSES.pending]: MEMBER_MESSAGES.pending, + [MEMBER_STATUSES.inactive]: MEMBER_MESSAGES.inactive, + [MEMBER_STATUSES.declined]: MEMBER_MESSAGES.declined, + [MEMBER_STATUSES.expired]: MEMBER_MESSAGES.expired, +} diff --git a/packages/components/modules/profiles/web/ProfileMembers/index.tsx b/packages/components/modules/profiles/web/ProfileMembers/index.tsx index 471aff38e..f9f6e210c 100644 --- a/packages/components/modules/profiles/web/ProfileMembers/index.tsx +++ b/packages/components/modules/profiles/web/ProfileMembers/index.tsx @@ -4,6 +4,7 @@ import { useCurrentProfile } from '@baseapp-frontend/authentication' import { LoadingState as DefaultLoadingState } from '@baseapp-frontend/design-system/components/web/displays' import { Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useLazyLoadQuery } from 'react-relay' import { UserMembersListPaginationQuery as UserMembersListPaginationQueryType } from '../../../../__generated__/UserMembersListPaginationQuery.graphql' @@ -31,22 +32,26 @@ const ProfileMembers: FC = ({ MembersListProps = {} }) => { } const ProfileMembersSuspended: FC = ({ - title = 'Members', + title, subtitle, InitialLoadingState = DefaultLoadingState, ...props -}) => ( - <> - - {title} - - - {subtitle} - - }> - - - -) +}) => { + const intl = useIntl() + + return ( + <> + + {title ?? intl.formatMessage({ id: 'profiles.members.title', defaultMessage: 'Members' })} + + + {subtitle} + + }> + + + + ) +} export default ProfileMembersSuspended diff --git a/packages/components/modules/profiles/web/ProfileSettingsComponent/constants.ts b/packages/components/modules/profiles/web/ProfileSettingsComponent/constants.ts new file mode 100644 index 000000000..ac089cf0e --- /dev/null +++ b/packages/components/modules/profiles/web/ProfileSettingsComponent/constants.ts @@ -0,0 +1,34 @@ +import { IntlShape, defineMessages } from 'react-intl' +import { z } from 'zod' + +import { DEFAULT_PROFILE_FORM_VALIDATION } from '../../common' + +export const PROFILE_SETTINGS_MESSAGES = defineMessages({ + nameEmpty: { + id: 'profiles.settings.validation.nameEmpty', + defaultMessage: 'Please enter a name.', + }, + urlPathEmpty: { + id: 'profiles.settings.validation.urlPathEmpty', + defaultMessage: 'Username must be at least 8 characters long.', + }, + urlPathInvalid: { + id: 'profiles.settings.validation.urlPathInvalid', + defaultMessage: 'Username can only contain letters and numbers', + }, + remove: { + id: 'profiles.settings.remove', + defaultMessage: 'Remove', + }, +}) + +export const getProfileFormValidationSchema = (intl: IntlShape) => + DEFAULT_PROFILE_FORM_VALIDATION.extend({ + name: z.string().min(1, { message: intl.formatMessage(PROFILE_SETTINGS_MESSAGES.nameEmpty) }), + urlPath: z + .string() + .min(8, { message: intl.formatMessage(PROFILE_SETTINGS_MESSAGES.urlPathEmpty) }) + .regex(/^[a-zA-Z0-9]+$/, { + message: intl.formatMessage(PROFILE_SETTINGS_MESSAGES.urlPathInvalid), + }), + }) diff --git a/packages/components/modules/profiles/web/ProfileSettingsComponent/index.tsx b/packages/components/modules/profiles/web/ProfileSettingsComponent/index.tsx index 232d8be31..9ab56b9c3 100644 --- a/packages/components/modules/profiles/web/ProfileSettingsComponent/index.tsx +++ b/packages/components/modules/profiles/web/ProfileSettingsComponent/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { FC, useEffect } from 'react' +import { FC, useEffect, useMemo } from 'react' import { useCurrentProfile } from '@baseapp-frontend/authentication' import { CircledAvatar } from '@baseapp-frontend/design-system/components/web/avatars' @@ -17,6 +17,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import LoadingButton from '@mui/lab/LoadingButton' import { Card, CardContent, InputAdornment, Typography } from '@mui/material' import { useForm } from 'react-hook-form' +import { FormattedMessage, useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { @@ -24,7 +25,6 @@ import { DEFAULT_BANNER_IMAGE_MAX_SIZE, DEFAULT_IMAGE_FORMATS, DEFAULT_IMAGE_MAX_SIZE, - DEFAULT_PROFILE_FORM_VALIDATION, PROFILE_FORM_VALUE, ProfileComponentFragment, ProfileUpdateForm, @@ -33,6 +33,7 @@ import { getProfileDefaultValues, useProfileMutation, } from '../../common' +import { PROFILE_SETTINGS_MESSAGES, getProfileFormValidationSchema } from './constants' import { AvatarUploadContainer, Banner, @@ -51,10 +52,12 @@ const ProfileSettingsComponent: FC = ({ profile: const { sendToast } = useNotification() const { updateProfileIfActive } = useCurrentProfile() + const intl = useIntl() + const validationSchema = useMemo(() => getProfileFormValidationSchema(intl), [intl]) const formReturn = useForm({ defaultValues: getProfileDefaultValues({ profile, removeSlashInUsername: true }), - resolver: zodResolver(DEFAULT_PROFILE_FORM_VALIDATION), + resolver: zodResolver(validationSchema), mode: 'onBlur', }) @@ -103,10 +106,22 @@ const ProfileSettingsComponent: FC = ({ profile: onCompleted: (response: any) => { const errors = response?.profileUpdate?.errors if (errors) { - sendToast('Something went wrong', { type: 'error' }) + sendToast( + intl.formatMessage({ + id: 'profiles.settings.updateError', + defaultMessage: 'Something went wrong', + }), + { type: 'error' }, + ) setFormRelayErrors(formReturn, errors) } else { - sendToast('Profile updated', { type: 'success' }) + sendToast( + intl.formatMessage({ + id: 'profiles.settings.updateSuccess', + defaultMessage: 'Profile updated', + }), + { type: 'success' }, + ) } }, }) @@ -143,10 +158,13 @@ const ProfileSettingsComponent: FC = ({ profile: >
- Profile + - Manage your personal information you and other people see. +
@@ -158,7 +176,10 @@ const ProfileSettingsComponent: FC = ({ profile: width={144} height={144} hasError={!!getFieldState('image').error} - alt="Avatar image" + alt={intl.formatMessage({ + id: 'profiles.settings.avatarAlt', + defaultMessage: 'Avatar image', + })} /> {getFieldState('image').error && ( @@ -173,7 +194,17 @@ const ProfileSettingsComponent: FC = ({ profile: setFile={setValue} accept={DEFAULT_IMAGE_FORMATS} maxSize={DEFAULT_IMAGE_MAX_SIZE} - label={hasUploadedImage ? 'Change Image' : 'Upload Image'} + label={ + hasUploadedImage + ? intl.formatMessage({ + id: 'profiles.settings.changeImage', + defaultMessage: 'Change Image', + }) + : intl.formatMessage({ + id: 'profiles.settings.uploadImage', + defaultMessage: 'Upload Image', + }) + } /> {watchImage && ( = ({ profile: loading={isMutationInFlight} disabled={isMutationInFlight} onClick={() => handleRemoveImage(PROFILE_FORM_VALUE.image)} - aria-label="Remove avatar button" + aria-label={intl.formatMessage({ + id: 'profiles.settings.removeAvatarAriaLabel', + defaultMessage: 'Remove avatar button', + })} > - Remove + {intl.formatMessage(PROFILE_SETTINGS_MESSAGES.remove)} )} @@ -192,13 +226,16 @@ const ProfileSettingsComponent: FC = ({ profile: = ({ profile: }} /> = ({ profile: = ({ profile: setFile={setValue} accept={DEFAULT_BANNER_IMAGE_FORMATS} maxSize={DEFAULT_BANNER_IMAGE_MAX_SIZE} - label={hasUploadedBannerImage ? 'Change Banner' : 'Upload Banner'} + label={ + hasUploadedBannerImage + ? intl.formatMessage({ + id: 'profiles.settings.changeBanner', + defaultMessage: 'Change Banner', + }) + : intl.formatMessage({ + id: 'profiles.settings.uploadBanner', + defaultMessage: 'Upload Banner', + }) + } sx={{ maxWidth: 'fit-content', justifySelf: 'end' }} /> {watchBannerImage && ( @@ -261,9 +314,12 @@ const ProfileSettingsComponent: FC = ({ profile: loading={isMutationInFlight} disabled={isMutationInFlight} sx={{ maxWidth: 'fit-content' }} - aria-label="Remove banner button" + aria-label={intl.formatMessage({ + id: 'profiles.settings.removeBannerAriaLabel', + defaultMessage: 'Remove banner button', + })} > - Remove + {intl.formatMessage(PROFILE_SETTINGS_MESSAGES.remove)} )} @@ -278,7 +334,7 @@ const ProfileSettingsComponent: FC = ({ profile: disabled={!isDirty || !isValid || isMutationInFlight} sx={{ maxWidth: 'fit-content', justifySelf: 'end' }} > - Save Changes + diff --git a/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/CreateProfileModal/constants.ts b/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/CreateProfileModal/constants.ts index 6cc0463d2..258894bb5 100644 --- a/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/CreateProfileModal/constants.ts +++ b/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/CreateProfileModal/constants.ts @@ -1,3 +1,4 @@ +import { IntlShape, defineMessages } from 'react-intl' import z from 'zod' const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/ @@ -13,3 +14,66 @@ export const schema = z.object({ .regex(slugRegex, 'Must contain only lowercase letters, numbers, and hyphens') .max(500, { message: 'Must have at most 500 characters' }), }) + +export const CREATE_PROFILE_MESSAGES = defineMessages({ + title: { + id: 'profiles.createProfile.title', + defaultMessage: 'New organization', + }, + description: { + id: 'profiles.createProfile.description', + defaultMessage: 'Create an organization and invite multiple members to manage and collaborate.', + }, + submit: { + id: 'profiles.createProfile.submit', + defaultMessage: 'Create Organization', + }, + nameMin: { + id: 'profiles.createProfile.validation.nameMin', + defaultMessage: 'Must have at least three character', + }, + nameMax: { + id: 'profiles.createProfile.validation.nameMax', + defaultMessage: 'Must have at most 255 characters', + }, + urlPathFormat: { + id: 'profiles.createProfile.validation.urlPathFormat', + defaultMessage: 'Must contain only lowercase letters, numbers, and hyphens', + }, + urlPathMax: { + id: 'profiles.createProfile.validation.urlPathMax', + defaultMessage: 'Must have at most 500 characters', + }, + urlPathTaken: { + id: 'profiles.createProfile.errors.urlPathTaken', + defaultMessage: 'This URL path is already taken. Please choose a different one.', + }, + nameTaken: { + id: 'profiles.createProfile.errors.nameTaken', + defaultMessage: + 'An organization with this name already exists. Please choose a different name.', + }, + urlPathInvalid: { + id: 'profiles.createProfile.errors.urlPathInvalid', + defaultMessage: + 'Invalid URL path format. Please use only lowercase letters, numbers, and hyphens.', + }, + createFailed: { + id: 'profiles.createProfile.errors.createFailed', + defaultMessage: + 'Unable to create organization. Please try again or contact support if the problem persists.', + }, +}) + +export const getCreateProfileSchema = (intl: IntlShape) => + z.object({ + name: z + .string() + .trim() + .min(3, { message: intl.formatMessage(CREATE_PROFILE_MESSAGES.nameMin) }) + .max(255, { message: intl.formatMessage(CREATE_PROFILE_MESSAGES.nameMax) }), + urlPath: z + .string() + .regex(slugRegex, intl.formatMessage(CREATE_PROFILE_MESSAGES.urlPathFormat)) + .max(500, { message: intl.formatMessage(CREATE_PROFILE_MESSAGES.urlPathMax) }), + }) diff --git a/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/CreateProfileModal/index.tsx b/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/CreateProfileModal/index.tsx index 257d3eb18..296a12ee5 100644 --- a/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/CreateProfileModal/index.tsx +++ b/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/CreateProfileModal/index.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react' +import { FC, ReactNode, useMemo } from 'react' import { CloseIcon } from '@baseapp-frontend/design-system/components/web/icons' import { TextField } from '@baseapp-frontend/design-system/components/web/inputs' @@ -19,26 +19,35 @@ import { Typography, } from '@mui/material' import { useForm } from 'react-hook-form' +import { FormattedMessage, useIntl } from 'react-intl' import { ConnectionHandler } from 'react-relay' import slugify from 'slugify' import { useOrganizationCreateMutation } from '../../../../common' -import { schema } from './constants' +import { CREATE_PROFILE_MESSAGES as MESSAGES, getCreateProfileSchema } from './constants' import { Form, TitleRow } from './styled' import { CreateProfileModalProps, OrganizationCreateForm } from './types' let nextClientMutationId = 0 +const renderTermsLink = (href: string) => (chunks: ReactNode[]) => ( + + {chunks} + +) + const CreateProfileModal: FC = ({ - addNewProfileLabel = 'New organization', + addNewProfileLabel, termsAndConditionsUrl = '', - addNewProfileDescription = 'Create an organization and invite multiple members to manage and collaborate.', - submitLabel = 'Create Organization', + addNewProfileDescription, + submitLabel, onClose, open, setOpen, userId, }) => { + const intl = useIntl() + const schema = useMemo(() => getCreateProfileSchema(intl), [intl]) const form = useForm({ mode: 'onChange', defaultValues: { @@ -64,26 +73,24 @@ const CreateProfileModal: FC = ({ if (errorMessage.includes('duplicate key') && errorMessage.includes('urlpath')) { return { field: 'urlPath', - message: 'This URL path is already taken. Please choose a different one.', + message: intl.formatMessage(MESSAGES.urlPathTaken), } } if (errorMessage.includes('duplicate key') && errorMessage.includes('name')) { return { field: 'name', - message: 'An organization with this name already exists. Please choose a different name.', + message: intl.formatMessage(MESSAGES.nameTaken), } } if (errorMessage.includes('invalid') && errorMessage.includes('urlpath')) { return { field: 'urlPath', - message: - 'Invalid URL path format. Please use only lowercase letters, numbers, and hyphens.', + message: intl.formatMessage(MESSAGES.urlPathInvalid), } } return { field: 'root', - message: - 'Unable to create organization. Please try again or contact support if the problem persists.', + message: intl.formatMessage(MESSAGES.createFailed), } } @@ -155,7 +162,9 @@ const CreateProfileModal: FC = ({ > - {addNewProfileLabel} + + {addNewProfileLabel ?? intl.formatMessage(MESSAGES.title)} + @@ -166,7 +175,9 @@ const CreateProfileModal: FC = ({ sx={{ display: 'flex', flexDirection: 'column', gap: 4 }} id="organization-modal-description" > - {addNewProfileDescription} + + {addNewProfileDescription ?? intl.formatMessage(MESSAGES.description)} + {form.formState.errors.root && ( form.clearErrors('root')}> @@ -176,7 +187,10 @@ const CreateProfileModal: FC = ({ = ({ } }} /> - + - Upon confirming, you agree to our{' '} - - Terms and Conditions. - + = ({ !form.formState.isValid } > - {submitLabel} + {submitLabel ?? intl.formatMessage(MESSAGES.submit)} diff --git a/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/index.tsx b/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/index.tsx index 85598ad9a..d51389d52 100644 --- a/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/index.tsx +++ b/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/index.tsx @@ -5,6 +5,7 @@ import { FC, Suspense, useState } from 'react' import { AddIcon } from '@baseapp-frontend/design-system/components/web/icons' import { ButtonBase, MenuItem, Stack } from '@mui/material' +import { useIntl } from 'react-intl' import { useLazyLoadQuery } from 'react-relay' import { AddProfilePopoverUserQuery as AddProfilePopoverUserQueryType } from '../../../../../__generated__/AddProfilePopoverUserQuery.graphql' @@ -13,9 +14,10 @@ import DefaultCreateProfileModal from './CreateProfileModal' import { AddProfileMenuItemProps } from './types' const AddProfileMenuItem: FC = ({ - addNewProfileLabel = 'New organization', + addNewProfileLabel, CreateProfileModal = DefaultCreateProfileModal, }) => { + const intl = useIntl() const [open, setOpen] = useState(false) const { me } = useLazyLoadQuery(AddProfilePopoverUserQuery, {}) @@ -30,7 +32,11 @@ const AddProfileMenuItem: FC = ({ sx={{ justifyContent: 'space-between' }} onClick={() => setOpen(true)} > - {addNewProfileLabel} + {addNewProfileLabel ?? + intl.formatMessage({ + id: 'profiles.createProfile.title', + defaultMessage: 'New organization', + })} diff --git a/packages/components/modules/profiles/web/profile-popover/CurrentProfile/index.tsx b/packages/components/modules/profiles/web/profile-popover/CurrentProfile/index.tsx index 0eafa791b..05a260e49 100644 --- a/packages/components/modules/profiles/web/profile-popover/CurrentProfile/index.tsx +++ b/packages/components/modules/profiles/web/profile-popover/CurrentProfile/index.tsx @@ -4,11 +4,13 @@ import { useCurrentProfile } from '@baseapp-frontend/authentication' import { AvatarWithPlaceholder } from '@baseapp-frontend/design-system/components/web/avatars' import { Box, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { Container } from './styled' const CurrentProfile: FC = () => { const { currentProfile: profile } = useCurrentProfile() + const intl = useIntl() if (!profile) return null @@ -18,7 +20,10 @@ const CurrentProfile: FC = () => { width={40} height={40} src={profile?.image ?? ''} - alt="Current profile avatar" + alt={intl.formatMessage({ + id: 'profiles.currentProfile.avatarAlt', + defaultMessage: 'Current profile avatar', + })} color="secondary" /> diff --git a/packages/components/modules/profiles/web/profile-popover/ProfilesList/EmptyState/index.tsx b/packages/components/modules/profiles/web/profile-popover/ProfilesList/EmptyState/index.tsx index 48a2dbaf7..f29ba9899 100644 --- a/packages/components/modules/profiles/web/profile-popover/ProfilesList/EmptyState/index.tsx +++ b/packages/components/modules/profiles/web/profile-popover/ProfilesList/EmptyState/index.tsx @@ -1,6 +1,7 @@ import { SearchingImage } from '@baseapp-frontend/design-system/components/web/illustrations' import { Box, Typography } from '@mui/material' +import { FormattedMessage } from 'react-intl' import { Container } from './styled' @@ -9,9 +10,17 @@ const EmptyState = () => ( - There are no profiles created. + + + - Your future profiles will be shown here. + diff --git a/packages/components/modules/profiles/web/profile-popover/ProfilesList/ProfileMenuItem/index.tsx b/packages/components/modules/profiles/web/profile-popover/ProfilesList/ProfileMenuItem/index.tsx index 510919020..f5e0ddac5 100644 --- a/packages/components/modules/profiles/web/profile-popover/ProfilesList/ProfileMenuItem/index.tsx +++ b/packages/components/modules/profiles/web/profile-popover/ProfilesList/ProfileMenuItem/index.tsx @@ -5,6 +5,7 @@ import { AvatarWithPlaceholder } from '@baseapp-frontend/design-system/component import { CheckMarkIcon } from '@baseapp-frontend/design-system/components/web/icons' import { Box, Typography } from '@mui/material' +import { useIntl } from 'react-intl' import { useFragment } from 'react-relay' import { ProfileItemFragment } from '../../../../common' @@ -19,6 +20,7 @@ const ProfileMenuItem: FC = ({ height = 36, }) => { const { currentProfile } = useCurrentProfile() + const intl = useIntl() const profile = useFragment(ProfileItemFragment, profileRef) if (!profile) { @@ -34,13 +36,26 @@ const ProfileMenuItem: FC = ({ tabIndex={0} active={isActiveProfile} onClick={() => onProfileChange(profile)} - aria-label={`Switch to ${profile.name ?? 'this profile'}`} + aria-label={ + profile.name + ? intl.formatMessage( + { id: 'profiles.profilesList.switchTo', defaultMessage: 'Switch to {name}' }, + { name: profile.name }, + ) + : intl.formatMessage({ + id: 'profiles.profilesList.switchToThisProfile', + defaultMessage: 'Switch to this profile', + }) + } > diff --git a/packages/components/modules/profiles/web/profile-popover/ProfilesList/index.tsx b/packages/components/modules/profiles/web/profile-popover/ProfilesList/index.tsx index b6fd89ff9..fe1f58477 100644 --- a/packages/components/modules/profiles/web/profile-popover/ProfilesList/index.tsx +++ b/packages/components/modules/profiles/web/profile-popover/ProfilesList/index.tsx @@ -7,6 +7,7 @@ import { ChevronIcon } from '@baseapp-frontend/design-system/components/web/icon import { useNotification } from '@baseapp-frontend/utils' import { Box, ButtonBase, Divider, Slide } from '@mui/material' +import { useIntl } from 'react-intl' import { useLazyLoadQuery, usePaginationFragment } from 'react-relay' import { Virtuoso } from 'react-virtuoso' @@ -35,6 +36,7 @@ const ProfilesList: FC = ({ }) const { sendToast } = useNotification() + const intl = useIntl() const { currentProfile, setCurrentProfile } = useCurrentProfile() const handleProfileChange = (profile: ProfileItemFragment$data) => { @@ -54,7 +56,12 @@ const ProfilesList: FC = ({ image: absoluteImagePath, urlPath: profile.urlPath?.path ?? null, }) - sendToast(`Switched to ${profile.name}`) + sendToast( + intl.formatMessage( + { id: 'profiles.profilesList.switchedTo', defaultMessage: 'Switched to {name}' }, + { name: profile.name }, + ), + ) handleCloseSubmenu() window.location.reload() } @@ -124,7 +131,14 @@ const ProfilesList: FC = ({ return ( - + {renderContent()} @@ -132,7 +146,8 @@ const ProfilesList: FC = ({ } const ProfilesListSuspended: FC = (props) => { - const { openSubmenu, handleCloseSubmenu, cancelLabel = 'Cancel' } = props + const { openSubmenu, handleCloseSubmenu, cancelLabel } = props + const intl = useIntl() return ( @@ -140,7 +155,7 @@ const ProfilesListSuspended: FC = (props) => { handleCloseSubmenu()}> - {cancelLabel} + {cancelLabel ?? intl.formatMessage({ id: 'common.cancel', defaultMessage: 'Cancel' })} diff --git a/packages/components/modules/profiles/web/profile-popover/SwitchProfileMenu/index.tsx b/packages/components/modules/profiles/web/profile-popover/SwitchProfileMenu/index.tsx index 4cb33dd74..f4d0a9ad4 100644 --- a/packages/components/modules/profiles/web/profile-popover/SwitchProfileMenu/index.tsx +++ b/packages/components/modules/profiles/web/profile-popover/SwitchProfileMenu/index.tsx @@ -3,12 +3,13 @@ import { FC } from 'react' import { ChevronIcon } from '@baseapp-frontend/design-system/components/web/icons' import { Box, ButtonBase, MenuItem, Stack } from '@mui/material' +import { FormattedMessage } from 'react-intl' import { SwitchProfileMenuProps } from './types' const SwitchProfileMenu: FC = ({ openProfilesList, - switchProfileLabel = 'Switch Profile', + switchProfileLabel, }) => ( @@ -18,7 +19,9 @@ const SwitchProfileMenu: FC = ({ sx={{ justifyContent: 'space-between' }} onClick={openProfilesList} > - {switchProfileLabel} + {switchProfileLabel ?? ( + + )} 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..785e1f1d7 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..1bb01a1c2 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)), + 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/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 index 0967ef424..c10e36820 100644 --- a/packages/design-system/locales/en.json +++ b/packages/design-system/locales/en.json @@ -1 +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 index 0967ef424..8438ca327 100644 --- a/packages/design-system/locales/es.json +++ b/packages/design-system/locales/es.json @@ -1 +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/pt.json b/packages/design-system/locales/pt.json index 0967ef424..19404afdd 100644 --- a/packages/design-system/locales/pt.json +++ b/packages/design-system/locales/pt.json @@ -1 +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/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/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 From 4be2748a46fbb0ca5ddbcc46d6c855a0b731112f Mon Sep 17 00:00:00 2001 From: Alisson Patricio Date: Fri, 9 Oct 2026 18:09:45 +0000 Subject: [PATCH 3/5] i18n: handle locale lookup failure and type MDXEditor interpolations Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FAfWR6vcsVFP1SxjTfyqD1 --- .../InitializedMDXEditor/index.tsx | 2 +- packages/i18n/providers/IntlProvider/index.tsx | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) 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 1bb01a1c2..e4e60f45f 100644 --- a/packages/design-system/components/web/inputs/MarkdownEditorField/InitializedMDXEditor/index.tsx +++ b/packages/design-system/components/web/inputs/MarkdownEditorField/InitializedMDXEditor/index.tsx @@ -46,7 +46,7 @@ const InitializedMDXEditor: FC = ({ 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)), + (value, [name, arg]) => value.replaceAll(`{{${name}}}`, String(arg as string | number)), defaultValue, ) }, diff --git a/packages/i18n/providers/IntlProvider/index.tsx b/packages/i18n/providers/IntlProvider/index.tsx index 45850c24b..92fe8a4e0 100644 --- a/packages/i18n/providers/IntlProvider/index.tsx +++ b/packages/i18n/providers/IntlProvider/index.tsx @@ -45,10 +45,13 @@ const IntlProviderWrapper: FC = ({ useEffect(() => { // Load locale from storage if not provided via props or cookies if (!initialLocale && !initialCookies) { - getCurrentLocale().then((currentLocale) => { - setLocale(currentLocale) - setMessages(getLocaleMessages(currentLocale)) - }) + getCurrentLocale() + .then((currentLocale) => { + setLocale(currentLocale) + setMessages(getLocaleMessages(currentLocale)) + }) + // Keep the default locale if the stored language can't be read. + .catch(() => undefined) } }, [initialLocale, initialCookies, getLocaleMessages]) From 43c695360a639df5add103264796b05f1ff97d5c Mon Sep 17 00:00:00 2001 From: Alisson Patricio Date: Sat, 10 Oct 2026 02:31:46 +0000 Subject: [PATCH 4/5] fix(payments): keep the locale's price part order; put the email inside the confirmation message Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FAfWR6vcsVFP1SxjTfyqD1 --- packages/components/locales/en.json | 2 +- packages/components/locales/es.json | 2 +- packages/components/locales/pt.json | 2 +- .../SubscriptionCard/constants.ts | 9 +++++++ .../SubscriptionCard/index.tsx | 21 ++++++--------- .../SubscriptionCard/types.ts | 6 +++++ .../SubscriptionCard/utils.ts | 26 +++++++++++++++++++ .../ConfirmationSubscriptionModal/index.tsx | 9 +++---- .../ConfirmationSubscriptionModal/utils.tsx | 3 +++ 9 files changed, 58 insertions(+), 22 deletions(-) create mode 100644 packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/constants.ts create mode 100644 packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/types.ts create mode 100644 packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/utils.ts create mode 100644 packages/components/modules/payments/web/CheckoutComponent/ConfirmationSubscriptionModal/utils.tsx diff --git a/packages/components/locales/en.json b/packages/components/locales/en.json index 11efccf02..85c25d9fe 100644 --- a/packages/components/locales/en.json +++ b/packages/components/locales/en.json @@ -226,7 +226,7 @@ "payments.checkout.unexpectedError": "An unexpected error occurred. Please try again.", "payments.checkout.updateFailed": "Failed to update subscription", "payments.confirmation.description": "Thank you for your subscription! Access the plan details by clicking on the button below", - "payments.confirmation.descriptionWithEmail": "Thank you for your subscription! Access the plan details by clicking on the button below or in the email we just sent to.", + "payments.confirmation.descriptionWithEmail": "Thank you for your subscription! Access the plan details by clicking on the button below or in the email we just sent to {email}.", "payments.confirmation.orderNumber": "Order Number:", "payments.confirmation.planDetails": "Plan Details", "payments.confirmation.title": "Successfully Subscribed", diff --git a/packages/components/locales/es.json b/packages/components/locales/es.json index 106e54210..ff96c6a5f 100644 --- a/packages/components/locales/es.json +++ b/packages/components/locales/es.json @@ -226,7 +226,7 @@ "payments.checkout.unexpectedError": "Ocurrió un error inesperado. Inténtalo de nuevo.", "payments.checkout.updateFailed": "No se pudo actualizar la suscripción", "payments.confirmation.description": "¡Gracias por tu suscripción! Accede a los detalles del plan haciendo clic en el botón de abajo", - "payments.confirmation.descriptionWithEmail": "¡Gracias por tu suscripción! Accede a los detalles del plan haciendo clic en el botón de abajo o en el correo electrónico que acabamos de enviar a.", + "payments.confirmation.descriptionWithEmail": "¡Gracias por tu suscripción! Accede a los detalles del plan haciendo clic en el botón de abajo o en el correo electrónico que acabamos de enviar a {email}.", "payments.confirmation.orderNumber": "Número de pedido:", "payments.confirmation.planDetails": "Detalles del plan", "payments.confirmation.title": "Suscripción exitosa", diff --git a/packages/components/locales/pt.json b/packages/components/locales/pt.json index 68dfc16ed..849cc738f 100644 --- a/packages/components/locales/pt.json +++ b/packages/components/locales/pt.json @@ -226,7 +226,7 @@ "payments.checkout.unexpectedError": "Ocorreu um erro inesperado. Tente novamente.", "payments.checkout.updateFailed": "Falha ao atualizar a assinatura", "payments.confirmation.description": "Obrigado pela sua assinatura! Acesse os detalhes do plano clicando no botão abaixo", - "payments.confirmation.descriptionWithEmail": "Obrigado pela sua assinatura! Acesse os detalhes do plano clicando no botão abaixo ou no e-mail que acabamos de enviar para.", + "payments.confirmation.descriptionWithEmail": "Obrigado pela sua assinatura! Acesse os detalhes do plano clicando no botão abaixo ou no e-mail que acabamos de enviar para {email}.", "payments.confirmation.orderNumber": "Número do pedido:", "payments.confirmation.planDetails": "Detalhes do plano", "payments.confirmation.title": "Assinatura realizada com sucesso", diff --git a/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/constants.ts b/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/constants.ts new file mode 100644 index 000000000..291b651cb --- /dev/null +++ b/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/constants.ts @@ -0,0 +1,9 @@ +import { TypographyProps } from '@mui/material' + +import { PricePartStyle } from './types' + +export const PRICE_SEGMENT_TYPOGRAPHY: Record = { + currency: { variant: 'h4', color: 'text.secondary' }, + major: { variant: 'h2' }, + minor: { variant: 'body1' }, +} diff --git a/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/index.tsx b/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/index.tsx index 15b063bea..2d49a1e03 100644 --- a/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/index.tsx +++ b/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/index.tsx @@ -8,6 +8,8 @@ import { FormattedMessage, useIntl } from 'react-intl' import { PAYMENTS_MESSAGES } from '../../constants' import { SubscriptionCardWrapper } from '../styled' import { SubscriptionCardProps } from '../types' +import { PRICE_SEGMENT_TYPOGRAPHY } from './constants' +import { getPriceSegments } from './utils' const SubscriptionCard = ({ sub, @@ -22,14 +24,7 @@ const SubscriptionCard = ({ style: 'currency', currency: 'USD', }) - const joinParts = (types: string[]) => - priceParts - .filter((part) => types.includes(part.type)) - .map((part) => part.value) - .join('') - const currencySymbol = joinParts(['currency']) - const dolarPrice = joinParts(['integer', 'group']) - const centsPrice = joinParts(['decimal', 'fraction']) + const priceSegments = getPriceSegments(priceParts) const marketingFeatures = sub.marketingFeatures ?? [] return ( @@ -49,11 +44,11 @@ const SubscriptionCard = ({ )} - - {currencySymbol} - - {dolarPrice} - {centsPrice} + {priceSegments.map(({ style, value }) => ( + + {value} + + ))} {selectedTerm === 'yearly' ? ( diff --git a/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/types.ts b/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/types.ts new file mode 100644 index 000000000..f4992271e --- /dev/null +++ b/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/types.ts @@ -0,0 +1,6 @@ +export type PricePartStyle = 'currency' | 'major' | 'minor' + +export interface PriceSegment { + style: PricePartStyle + value: string +} diff --git a/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/utils.ts b/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/utils.ts new file mode 100644 index 000000000..cb2364231 --- /dev/null +++ b/packages/components/modules/payments/web/AvailableSubscriptions/SubscriptionCard/utils.ts @@ -0,0 +1,26 @@ +import { PricePartStyle, PriceSegment } from './types' + +const STYLE_BY_PART_TYPE: Partial> = { + currency: 'currency', + integer: 'major', + group: 'major', + decimal: 'minor', + fraction: 'minor', +} + +/** Groups formatted price parts by display style, keeping the locale's order and dropping + * whitespace literals (the layout spaces the segments). */ +export const getPriceSegments = (parts: Intl.NumberFormatPart[]): PriceSegment[] => + parts.reduce((segments, part) => { + const style = STYLE_BY_PART_TYPE[part.type] + const last = segments[segments.length - 1] + if (!style) { + if (part.value.trim() && last) last.value += part.value + return segments + } + if (last?.style === style) { + last.value += part.value + return segments + } + return [...segments, { style, value: part.value }] + }, []) diff --git a/packages/components/modules/payments/web/CheckoutComponent/ConfirmationSubscriptionModal/index.tsx b/packages/components/modules/payments/web/CheckoutComponent/ConfirmationSubscriptionModal/index.tsx index 7822ae624..0231887d1 100644 --- a/packages/components/modules/payments/web/CheckoutComponent/ConfirmationSubscriptionModal/index.tsx +++ b/packages/components/modules/payments/web/CheckoutComponent/ConfirmationSubscriptionModal/index.tsx @@ -11,6 +11,7 @@ import { FormattedMessage, useIntl } from 'react-intl' import { maskEmail } from '../utils' import { ConfirmationSubscriptionModalProps } from './types' +import { renderEmphasis } from './utils' const ConfirmationSubscriptionModal: FC = ({ open, @@ -37,7 +38,8 @@ const ConfirmationSubscriptionModal: FC = ({ {user?.email ? ( ) : ( = ({ )} - - - {user?.email && maskedEmail} - - {orderNumber && ( {chunks} From f05775fd9b881fde87932f9fd2eb9eac84adb87c Mon Sep 17 00:00:00 2001 From: Alisson Patricio Date: Sat, 17 Oct 2026 17:20:26 +0000 Subject: [PATCH 5/5] chore(profiles): drop unused English create-profile schema Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FAfWR6vcsVFP1SxjTfyqD1 --- .../CreateProfileModal/constants.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/CreateProfileModal/constants.ts b/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/CreateProfileModal/constants.ts index 258894bb5..0449175e2 100644 --- a/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/CreateProfileModal/constants.ts +++ b/packages/components/modules/profiles/web/profile-popover/AddProfileMenuItem/CreateProfileModal/constants.ts @@ -3,18 +3,6 @@ import z from 'zod' const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/ -export const schema = z.object({ - name: z - .string() - .trim() - .min(3, { message: 'Must have at least three character' }) - .max(255, { message: 'Must have at most 255 characters' }), - urlPath: z - .string() - .regex(slugRegex, 'Must contain only lowercase letters, numbers, and hyphens') - .max(500, { message: 'Must have at most 500 characters' }), -}) - export const CREATE_PROFILE_MESSAGES = defineMessages({ title: { id: 'profiles.createProfile.title',