-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
setup localization for twenty-emails #9806
Changes from 17 commits
54c3677
1b5ab85
09858d6
c6a7204
9d6cc8d
3a38c8b
8527463
ba57e71
00ac7c9
f67d60a
18ad5f6
20d075c
67c7fc2
1f85f43
c927341
75d0eb6
e3689ac
208a8c5
246f784
1c5018e
afc284c
36a94f1
ca1fad3
d7a861d
c25ea73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"$schema": "https://json.schemastore.org/swcrc", | ||
"jsc": { | ||
"experimental": { | ||
"plugins": [ | ||
[ | ||
"@lingui/swc-plugin", | ||
{ | ||
"runtimeModules": { | ||
"i18n": ["@lingui/core", "i18n"], | ||
"trans": ["@lingui/react", "Trans"] | ||
} | ||
} | ||
Comment on lines
+8
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: indentation is inconsistent with the rest of the file (extra space before opening brace) |
||
] | ||
] | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { defineConfig } from '@lingui/cli'; | ||
|
||
export default defineConfig({ | ||
sourceLocale: 'en', | ||
locales: ['fr', 'en', 'pt', 'de', 'it', 'es', 'zh-Hans', 'zh-Hant'], | ||
catalogs: [ | ||
{ | ||
path: '<rootDir>/src/locales/{locale}/messages', | ||
include: ['src'], | ||
}, | ||
], | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,26 @@ | ||
import { PropsWithChildren } from 'react'; | ||
import { i18n } from '@lingui/core'; | ||
import { I18nProvider } from '@lingui/react'; | ||
import { Container, Html } from '@react-email/components'; | ||
import { PropsWithChildren } from 'react'; | ||
|
||
import { BaseHead } from 'src/components/BaseHead'; | ||
import { Logo } from 'src/components/Logo'; | ||
|
||
type BaseEmailProps = PropsWithChildren<{ | ||
width?: number; | ||
locale: string; | ||
}>; | ||
|
||
export const BaseEmail = ({ children, width }: BaseEmailProps) => { | ||
export const BaseEmail = ({ children, width, locale }: BaseEmailProps) => { | ||
return ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: locale prop is now required but no default value provided - could cause runtime errors if not passed |
||
<Html lang="en"> | ||
<BaseHead /> | ||
<Container width={width || 290}> | ||
<Logo /> | ||
{children} | ||
</Container> | ||
</Html> | ||
<I18nProvider i18n={i18n}> | ||
<Html lang={locale}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: i18n instance needs to be initialized and configured before being used in I18nProvider |
||
<BaseHead /> | ||
<Container width={width || 290}> | ||
<Logo /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: width default of 290 should be defined as a constant at the top level |
||
{children} | ||
</Container> | ||
</Html> | ||
</I18nProvider> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -17,7 +17,7 @@ export const CleanSuspendedWorkspaceEmail = ({ | |||||
const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello'; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: helloString needs to be wrapped in <Trans> component for localization |
||||||
|
||||||
return ( | ||||||
<BaseEmail width={333}> | ||||||
<BaseEmail width={333} locale="en"> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: locale="en" should not be hardcoded - should be passed as a prop from parent component
Suggested change
|
||||||
<Title value="Deleted Workspace 🥺" /> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Title text needs to be wrapped in <Trans> component for localization |
||||||
<MainText> | ||||||
{helloString}, | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,35 @@ | ||
import { t } from '@lingui/core/macro'; | ||
import { Trans } from '@lingui/react/macro'; | ||
import { BaseEmail } from 'src/components/BaseEmail'; | ||
import { CallToAction } from 'src/components/CallToAction'; | ||
import { Link } from 'src/components/Link'; | ||
import { MainText } from 'src/components/MainText'; | ||
import { Title } from 'src/components/Title'; | ||
import { loadAndActivateLocale } from 'src/utils/loadAndActivateLocale'; | ||
|
||
type PasswordResetLinkEmailProps = { | ||
duration: string; | ||
link: string; | ||
locale: string; | ||
}; | ||
|
||
export const PasswordResetLinkEmail = ({ | ||
duration, | ||
link, | ||
locale, | ||
}: PasswordResetLinkEmailProps) => { | ||
loadAndActivateLocale(locale); | ||
|
||
return ( | ||
<BaseEmail> | ||
<Title value="Reset your password 🗝" /> | ||
<CallToAction href={link} value="Reset" /> | ||
<BaseEmail locale={locale}> | ||
<Title value={t`Reset your password 🗝`} /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: The emoji in the title string may cause display issues in some email clients. Consider using a Unicode emoji or removing it for better compatibility. |
||
<CallToAction href={link} value={t`Reset`} /> | ||
<MainText> | ||
This link is only valid for the next {duration}. If link does not work, | ||
you can use the login verification link directly: | ||
<br /> | ||
<Trans> | ||
This link is only valid for the next {duration}. If the link does not | ||
work, you can use the login verification link directly: | ||
Comment on lines
+27
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: The duration variable is not wrapped in a translation component, which is why it remains in English across all locales. Consider using a number + unit format that can be properly localized. |
||
<br /> | ||
</Trans> | ||
<Link href={link} value={link} /> | ||
</MainText> | ||
</BaseEmail> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,49 @@ | ||
import { format } from 'date-fns'; | ||
import { i18n } from '@lingui/core'; | ||
import { t } from '@lingui/core/macro'; | ||
import { Trans } from '@lingui/react/macro'; | ||
|
||
import { BaseEmail } from 'src/components/BaseEmail'; | ||
import { CallToAction } from 'src/components/CallToAction'; | ||
import { MainText } from 'src/components/MainText'; | ||
import { Title } from 'src/components/Title'; | ||
import { loadAndActivateLocale } from 'src/utils/loadAndActivateLocale'; | ||
|
||
type PasswordUpdateNotifyEmailProps = { | ||
userName: string; | ||
email: string; | ||
link: string; | ||
locale: string; | ||
}; | ||
|
||
export const PasswordUpdateNotifyEmail = ({ | ||
userName, | ||
email, | ||
link, | ||
locale, | ||
}: PasswordUpdateNotifyEmailProps) => { | ||
const helloString = userName?.length > 1 ? `Dear ${userName}` : 'Dear'; | ||
loadAndActivateLocale(locale); | ||
|
||
const helloString = userName?.length > 1 ? t`Dear ${userName}` : t`Dear`; | ||
const formattedDate = i18n.date(new Date()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: string interpolation in t macro may not work as expected - consider using Trans component instead for variable interpolation |
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: i18n.date() needs date format options to ensure consistent formatting across locales |
||
return ( | ||
<BaseEmail> | ||
<Title value="Password updated" /> | ||
<BaseEmail locale={locale}> | ||
<Title value={t`Password updated`} /> | ||
<MainText> | ||
{helloString}, | ||
<br /> | ||
<br /> | ||
This is a confirmation that password for your account ({email}) was | ||
successfully changed on {format(new Date(), 'MMMM d, yyyy')}. | ||
<br /> | ||
<br /> | ||
If you did not initiate this change, please contact your workspace owner | ||
immediately. | ||
<Trans> | ||
This is a confirmation that password for your account ({email}) was | ||
successfully changed on {formattedDate}. | ||
<br /> | ||
<br /> | ||
If you did not initiate this change, please contact your workspace | ||
owner immediately. | ||
</Trans> | ||
<br /> | ||
</MainText> | ||
<CallToAction value="Connect to Twenty" href={link} /> | ||
<CallToAction value={t`Connect to Twenty`} href={link} /> | ||
</BaseEmail> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -1,3 +1,5 @@ | ||||||||
import { t } from '@lingui/core/macro'; | ||||||||
import { Trans } from '@lingui/react/macro'; | ||||||||
import { Img } from '@react-email/components'; | ||||||||
import { emailTheme } from 'src/common-style'; | ||||||||
|
||||||||
|
@@ -10,6 +12,7 @@ import { MainText } from 'src/components/MainText'; | |||||||
import { Title } from 'src/components/Title'; | ||||||||
import { WhatIsTwenty } from 'src/components/WhatIsTwenty'; | ||||||||
import { capitalize } from 'src/utils/capitalize'; | ||||||||
import { loadAndActivateLocale } from 'src/utils/loadAndActivateLocale'; | ||||||||
import { getImageAbsoluteURI } from 'twenty-shared'; | ||||||||
|
||||||||
type SendInviteLinkEmailProps = { | ||||||||
|
@@ -21,35 +24,40 @@ type SendInviteLinkEmailProps = { | |||||||
lastName: string; | ||||||||
}; | ||||||||
serverUrl: string; | ||||||||
locale: string; | ||||||||
}; | ||||||||
|
||||||||
export const SendInviteLinkEmail = ({ | ||||||||
link, | ||||||||
workspace, | ||||||||
sender, | ||||||||
serverUrl, | ||||||||
locale, | ||||||||
}: SendInviteLinkEmailProps) => { | ||||||||
loadAndActivateLocale(locale); | ||||||||
|
||||||||
const workspaceLogo = workspace.logo | ||||||||
? getImageAbsoluteURI({ imageUrl: workspace.logo, baseUrl: serverUrl }) | ||||||||
: null; | ||||||||
|
||||||||
return ( | ||||||||
<BaseEmail width={333}> | ||||||||
<Title value="Join your team on Twenty" /> | ||||||||
<BaseEmail width={333} locale={locale}> | ||||||||
<Title value={t`Join your team on Twenty`} /> | ||||||||
<MainText> | ||||||||
{capitalize(sender.firstName)} ( | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: sender.firstName needs to be wrapped in t`` for translation since it's being capitalized |
||||||||
<Link | ||||||||
href={`mailto:${sender.email}`} | ||||||||
value={sender.email} | ||||||||
color={emailTheme.font.colors.blue} | ||||||||
/> | ||||||||
) has invited you to join a workspace called <b>{workspace.name}</b> | ||||||||
)<Trans>has invited you to join a workspace called </Trans> | ||||||||
<b>{workspace.name}</b> | ||||||||
Comment on lines
+50
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: text split between Trans component and workspace name may cause grammatical issues in languages with different word orders
Suggested change
|
||||||||
<br /> | ||||||||
</MainText> | ||||||||
<HighlightedContainer> | ||||||||
{workspaceLogo && <Img src={workspaceLogo} width={40} height={40} />} | ||||||||
{workspace.name && <HighlightedText value={workspace.name} />} | ||||||||
<CallToAction href={link} value="Accept invite" /> | ||||||||
<CallToAction href={link} value={t`Accept invite`} /> | ||||||||
</HighlightedContainer> | ||||||||
<WhatIsTwenty /> | ||||||||
</BaseEmail> | ||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,7 @@ export const WarnSuspendedWorkspaceEmail = ({ | |
const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: 'Hello' string should be localized using Trans or t macro |
||
|
||
return ( | ||
<BaseEmail width={333}> | ||
<BaseEmail width={333} locale="en"> | ||
<Title value="Suspended Workspace 😴" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: title value should be localized using t macro |
||
<MainText> | ||
{helloString}, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
msgid "" | ||
msgstr "" | ||
"POT-Creation-Date: 2025-01-21 22:51+0530\n" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: POT-Creation-Date is set to 2025-01-21, which is in the future. This should be the current date. |
||
"MIME-Version: 1.0\n" | ||
"Content-Type: text/plain; charset=utf-8\n" | ||
"Content-Transfer-Encoding: 8bit\n" | ||
"X-Generator: @lingui/cli\n" | ||
"Language: de\n" | ||
"Project-Id-Version: \n" | ||
"Report-Msgid-Bugs-To: \n" | ||
"PO-Revision-Date: \n" | ||
"Last-Translator: \n" | ||
"Language-Team: \n" | ||
"Plural-Forms: \n" | ||
|
||
|
||
#: src/emails/send-invite-link.email.tsx:60 | ||
msgid "Accept invite" | ||
msgstr "Einladung annehmen" | ||
|
||
#: src/emails/send-email-verification-link.email.tsx:24 | ||
msgid "Confirm your email address" | ||
msgstr "Bestätigen Sie Ihre E-Mail-Adresse" | ||
|
||
#: src/emails/password-update-notify.email.tsx:46 | ||
msgid "Connect to Twenty" | ||
msgstr "Verbinde dich mit Twenty" | ||
|
||
#: src/emails/password-update-notify.email.tsx:26 | ||
msgid "Dear" | ||
msgstr "Sehr geehrte/r" | ||
|
||
#: src/emails/password-update-notify.email.tsx:26 | ||
msgid "Dear {userName}" | ||
msgstr "Sehr geehrte/r {userName}" | ||
|
||
#: src/emails/send-invite-link.email.tsx:53 | ||
msgid "has invited you to join a workspace called " | ||
msgstr "hat Sie eingeladen, einem Arbeitsbereich namens " | ||
|
||
#: src/emails/send-invite-link.email.tsx:45 | ||
msgid "Join your team on Twenty" | ||
msgstr "Treten Sie Ihrem Team auf Twenty bei" | ||
|
||
#: src/emails/password-update-notify.email.tsx:31 | ||
msgid "Password updated" | ||
msgstr "Passwort aktualisiert" | ||
|
||
#: src/emails/password-reset-link.email.tsx:26 | ||
msgid "Reset" | ||
msgstr "Zurücksetzen" | ||
|
||
#: src/emails/password-reset-link.email.tsx:25 | ||
msgid "Reset your password 🗝" | ||
msgstr "Setzen Sie Ihr Passwort zurück 🗝" | ||
|
||
#: src/emails/send-email-verification-link.email.tsx:29 | ||
msgid "Thanks for registering for an account on Twenty! Before we get started, we just need to confirm that this is you. Click above to verify your email address." | ||
msgstr "Danke, dass Sie sich für ein Konto auf Twenty registriert haben! Bevor wir beginnen, müssen wir nur bestätigen, dass Sie es sind. Klicken Sie oben, um Ihre E-Mail-Adresse zu bestätigen." | ||
|
||
#: src/emails/password-update-notify.email.tsx:36 | ||
msgid "This is a confirmation that password for your account ({email}) was successfully changed on {formattedDate}.<0/><1/>If you did not initiate this change, please contact your workspace owner immediately." | ||
msgstr "Dies ist eine Bestätigung, dass das Passwort für Ihr Konto ({email}) erfolgreich am {formattedDate} geändert wurde.<0/><1/>Wenn Sie diese Änderung nicht initiiert haben, kontaktieren Sie bitte sofort den Eigentümer Ihres Arbeitsbereichs." | ||
|
||
#: src/emails/password-reset-link.email.tsx:28 | ||
msgid "This link is only valid for the next {duration}. If the link does not work, you can use the login verification link directly:<0/>" | ||
msgstr "Dieser Link ist nur für die nächsten {duration} gültig. Wenn der Link nicht funktioniert, können Sie direkt den Login-Verifizierungslink verwenden:<0/>" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: The {duration} variable is not localized and will always appear in English as noted in the PR description. Consider using a localized duration format. |
||
|
||
#: src/emails/send-email-verification-link.email.tsx:25 | ||
msgid "Verify Email" | ||
msgstr "E-Mail verifizieren" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"A7zEzZ\":[[\"daysLeft\",\"plural\",{\"one\":[\"Tag übrig\"],\"other\":[\"Tage übrig\"]}]],\"0JB+B5\":[[\"daysLeft\",\"plural\",{\"one\":[\"Keine Sorge! Erstellen oder bearbeiten Sie einfach einen Datensatz innerhalb des nächsten \",[\"remainingDays\"],\" Tages, um weiterhin Zugriff zu haben.\"],\"other\":[\"Keine Sorge! Erstellen oder bearbeiten Sie einfach einen Datensatz innerhalb der nächsten \",[\"remainingDays\"],\" Tage, um weiterhin Zugriff zu haben.\"]}]],\"4WPI3S\":[\"Einladung annehmen\"],\"RPHFhC\":[\"Bestätigen Sie Ihre E-Mail-Adresse\"],\"nvkBPN\":[\"Verbinde dich mit Twenty\"],\"791xh3\":[\"Inaktive Arbeitsbereiche 😵, die gelöscht werden sollten\"],\"JRzgV7\":[\"Sehr geehrte/r\"],\"Lm5BBI\":[\"Sehr geehrte/r \",[\"userName\"]],\"tGme7M\":[\"hat Sie eingeladen, einem Arbeitsbereich namens \"],\"uzTaYi\":[\"Hallo\"],\"r0OJlV\":[\"Hallo \",[\"userName\"]],\"vq4f+3\":[\"Inaktiver Arbeitsbereich 😴\"],\"Y4Nk3c\":[\"Es scheint eine Inaktivitätsperiode in Ihrem<0>\",[\"workspaceDisplayName\"],\"</0> Arbeitsbereich gegeben zu haben.<1/><2/>Bitte beachten Sie, dass das Konto bald deaktiviert wird und alle damit verbundenen Daten in diesem Arbeitsbereich gelöscht werden.<3/><4/>\"],\"PviVyk\":[\"Treten Sie Ihrem Team auf Twenty bei\"],\"ja42wz\":[\"Liste der <0>workspaceIds</0>, die seit mindestens <1>\",[\"minDaysSinceInactive\"],\" Tagen inaktiv sind:\"],\"ogtYkT\":[\"Passwort aktualisiert\"],\"OfhWJH\":[\"Zurücksetzen\"],\"RE5NiU\":[\"Setzen Sie Ihr Passwort zurück 🗝\"],\"7yDt8q\":[\"Danke, dass Sie sich für ein Konto auf Twenty registriert haben! Bevor wir beginnen, müssen wir nur bestätigen, dass Sie es sind. Klicken Sie oben, um Ihre E-Mail-Adresse zu bestätigen.\"],\"wSOsS+\":[\"Dies ist eine Bestätigung, dass das Passwort für Ihr Konto (\",[\"email\"],\") erfolgreich am \",[\"formattedDate\"],\" geändert wurde.<0/><1/>Wenn Sie diese Änderung nicht initiiert haben, kontaktieren Sie bitte sofort den Eigentümer Ihres Arbeitsbereichs.\"],\"RGs4CL\":[\"This link is only valid for the next \",[\"duration\"],\". If the link does not\\n work, you can use the login verification link directly:<0/>\"],\"2oA637\":[\"Dieser Link ist nur für die nächsten \",[\"duration\"],\" gültig. Wenn der Link nicht funktioniert, können Sie direkt den Login-Verifizierungslink verwenden:<0/>\"],\"wCKkSr\":[\"E-Mail verifizieren\"]}")as Messages; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see Babel in the emails folder. I think it's best to use SWC but it seemed to use Babel. But you didn't migrate twenty-emails to SWC here did you? I'm surprised it works. Want to take a shot at removing Babel?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I didn't migrated to SWC. I chose to go creating
.swcrc
because thevite.config.ts
already imported React from@vitejs/plugin-react-swc
I will remove babel and will check how it goes then.