chore: ttag setup and authentication flow localization (#2987)

* chore: add ttag dependency

* chore: wrap strings for authentication flows

* chore: fix lint errors
This commit is contained in:
Antonella Sgarlatta
2026-03-05 23:46:58 -03:00
committed by GitHub
parent 92b7be4221
commit 7f3d2d0c72
12 changed files with 111 additions and 57 deletions

View File

@@ -120,6 +120,7 @@
"comlink": "^4.4.1",
"fast-diff": "^1.3.0",
"lexical": "0.38.1",
"ttag": "^1.8.12",
"unicode-script": "^1.2.0"
}
}

View File

@@ -20,6 +20,7 @@ import { useCaptcha } from '@/Hooks/useCaptcha'
import { isErrorResponse } from '@standardnotes/snjs'
import MergeLocalDataCheckbox from './MergeLocalDataCheckbox'
import ConfirmNoMergeDialog from './ConfirmNoMergeDialog'
import { c } from 'ttag'
type Props = {
setMenuPane: (pane: AccountMenuPane) => void
@@ -165,9 +166,9 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
const confirmPasswordForm = (
<>
<div className="mb-3 px-3 text-sm">
Because your notes are encrypted using your password,{' '}
<span className="text-danger">Standard Notes does not have a password reset option</span>. If you forget your
password, you will permanently lose access to your data.
{c('Info').jt`Because your notes are encrypted using your password, ${(
<span className="text-danger">Standard Notes does not have a password reset option</span>
)}. If you forget your password, you will permanently lose access to your data.`}
</div>
<form onSubmit={handleConfirmFormSubmit} className="mb-1 px-3">
{!isRegistering && (
@@ -177,7 +178,7 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
left={[<Icon type="password" className="text-neutral" />]}
onChange={handlePasswordChange}
onKeyDown={handleKeyDown}
placeholder="Confirm password"
placeholder={c('Label').t`Confirm password`}
ref={passwordInputRef}
value={confirmPassword}
/>
@@ -187,13 +188,13 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
primary
fullWidth
className="mb-3 mt-1"
label={isRegistering ? 'Creating account...' : 'Create account & sign in'}
label={isRegistering ? c('Action').t`Creating account...` : c('Action').t`Create account & sign in`}
onClick={handleConfirmFormSubmit}
disabled={isRegistering}
/>
<Checkbox
name="is-ephemeral"
label="Stay signed in"
label={c('Option').t`Stay signed in`}
checked={!isEphemeral}
onChange={handleEphemeralChange}
disabled={isRegistering}
@@ -215,13 +216,15 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
<div className="mb-3 mt-1 flex items-center px-3">
<IconButton
icon="arrow-left"
title="Go back"
title={c('Action').t`Go back`}
className="mr-2 flex p-0 text-neutral"
onClick={handleGoBack}
focusable={true}
disabled={isRegistering}
/>
<div className="text-base font-bold">{captchaURL ? 'Human verification' : 'Confirm password'}</div>
<div className="text-base font-bold">
{captchaURL ? c('Title').t`Human verification` : c('Title').t`Confirm password`}
</div>
</div>
{captchaURL ? <div className="p-[10px]">{captchaIframe}</div> : confirmPasswordForm}
{showNoMergeConfirmation && (

View File

@@ -16,6 +16,7 @@ import Icon from '@/Components/Icon/Icon'
import IconButton from '@/Components/Button/IconButton'
import AdvancedOptions from './AdvancedOptions'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { c } from 'ttag'
type Props = {
setMenuPane: (pane: AccountMenuPane) => void
@@ -101,12 +102,12 @@ const CreateAccount: FunctionComponent<Props> = ({ setMenuPane, email, setEmail,
<div className="mb-3 mt-1 flex items-center px-3">
<IconButton
icon="arrow-left"
title="Go back"
title={c('Action').t`Go back`}
className="mr-2 flex p-0 text-neutral"
onClick={handleClose}
focusable={true}
/>
<div className="text-base font-bold">Create account</div>
<div className="text-base font-bold">{c('Title').t`Create account`}</div>
</div>
<form onSubmit={handleRegisterFormSubmit} className="mb-1 px-3">
<DecoratedInput
@@ -115,7 +116,7 @@ const CreateAccount: FunctionComponent<Props> = ({ setMenuPane, email, setEmail,
left={[<Icon type="email" className="text-neutral" />]}
onChange={handleEmailChange}
onKeyDown={handleKeyDown}
placeholder="Email"
placeholder={c('Label').t`Email`}
ref={emailInputRef}
type="email"
value={email}
@@ -126,11 +127,17 @@ const CreateAccount: FunctionComponent<Props> = ({ setMenuPane, email, setEmail,
left={[<Icon type="password" className="text-neutral" />]}
onChange={handlePasswordChange}
onKeyDown={handleKeyDown}
placeholder="Password"
placeholder={c('Label').t`Password`}
ref={passwordInputRef}
value={password}
/>
<Button className="mt-1" label="Next" primary onClick={handleRegisterFormSubmit} fullWidth={true} />
<Button
className="mt-1"
label={c('Action').t`Next`}
primary
onClick={handleRegisterFormSubmit}
fullWidth={true}
/>
</form>
<HorizontalSeparator classes="my-2" />
<AdvancedOptions onPrivateUsernameModeChange={onPrivateUsernameChange} />

View File

@@ -15,6 +15,7 @@ import { useApplication } from '../ApplicationProvider'
import MenuSection from '../Menu/MenuSection'
import { TOGGLE_COMMAND_PALETTE, TOGGLE_KEYBOARD_SHORTCUTS_MODAL, isMobilePlatform } from '@standardnotes/ui-services'
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
import { c } from 'ttag'
type Props = {
mainApplicationGroup: WebApplicationGroup
@@ -138,12 +139,12 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
<>
<div className="mb-1 px-4 md:px-3">
<div className="mb-3 text-base text-foreground lg:text-sm">
Youre offline. Sign in to sync your notes and preferences across all your devices and enable end-to-end
encryption.
{c('Info')
.t`Youre offline. Sign in to sync your notes and preferences across all your devices and enable end-to-end encryption.`}
</div>
<div className="flex items-center text-passive-1">
<Icon type="cloud-off" className={`mr-2 ${MenuItemIconSize}`} />
<span className="text-lg font-semibold lg:text-sm">Offline</span>
<span className="text-lg font-semibold lg:text-sm">{c('Status').t`Offline`}</span>
</div>
</div>
</>
@@ -166,11 +167,11 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
<>
<MenuItem onClick={activateRegisterPane}>
<Icon type="user" className={iconClassName} />
Create free account
{c('Action').t`Create free account`}
</MenuItem>
<MenuItem onClick={activateSignInPane}>
<Icon type="signIn" className={iconClassName} />
Sign in
{c('Action').t`Sign in`}
</MenuItem>
</>
)}

View File

@@ -15,6 +15,7 @@ import { useApplication } from '../ApplicationProvider'
import { useCaptcha } from '@/Hooks/useCaptcha'
import MergeLocalDataCheckbox from './MergeLocalDataCheckbox'
import ConfirmNoMergeDialog from './ConfirmNoMergeDialog'
import { c } from 'ttag'
type Props = {
setMenuPane: (pane: AccountMenuPane) => void
@@ -252,7 +253,7 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
className={{ container: `mb-2 ${error ? 'border-danger' : null}` }}
left={[<Icon type="email" className="text-neutral" />]}
type="email"
placeholder="Email"
placeholder={c('Label').t`Email`}
value={email}
onChange={handleEmailChange}
onFocus={resetInvalid}
@@ -268,14 +269,14 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
onChange={handlePasswordChange}
onFocus={resetInvalid}
onKeyDown={handleKeyDown}
placeholder="Password"
placeholder={c('Label').t`Password`}
ref={passwordInputRef}
value={password}
/>
{error ? <div className="my-2 text-danger">{error}</div> : null}
<Button
className="mb-3 mt-1"
label={isSigningIn ? 'Signing in...' : 'Sign in'}
label={isSigningIn ? c('Action').t`Signing in...` : c('Action').t`Sign in`}
primary
onClick={handleSignInFormSubmit}
disabled={isSigningIn}
@@ -283,7 +284,7 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
/>
<Checkbox
name="is-ephemeral"
label="Stay signed in"
label={c('Option').t`Stay signed in`}
checked={!isEphemeral}
disabled={isSigningIn || isRecoverySignIn}
onChange={handleEphemeralChange}
@@ -325,13 +326,15 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
<div className="mb-3 mt-1 flex items-center px-3">
<IconButton
icon="arrow-left"
title="Go back"
title={c('Action').t`Go back`}
className="mr-2 flex p-0 text-neutral"
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
focusable={true}
disabled={isSigningIn}
/>
<div className="text-base font-bold">{showCaptcha ? 'Human verification' : 'Sign in'}</div>
<div className="text-base font-bold">
{showCaptcha ? c('Title').t`Human verification` : c('Title').t`Sign in`}
</div>
</div>
{showCaptcha ? <div className="p-[10px]">{captchaIframe}</div> : signInForm}
{showNoMergeConfirmation && (

View File

@@ -7,6 +7,7 @@ import { AccountIllustration } from '@standardnotes/icons'
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import { c } from 'ttag'
type Props = {
application: WebApplication
@@ -25,22 +26,24 @@ const Authentication: FunctionComponent<Props> = ({ application }) => {
application.accountMenuController.setShow(true)
}
const loginLink = (
<button className="cursor-pointer border-0 bg-default p-0 text-info underline" onClick={clickSignIn}>
{c('Action').t`Sign in`}
</button>
)
return (
<PreferencesGroup>
<PreferencesSegment>
<div className="flex flex-col items-center px-4 md:px-12">
<AccountIllustration className="mb-3" />
<Title>You're not signed in</Title>
<Title>{c('Title').t`You're not signed in`}</Title>
<div className="mb-3 text-center text-base lg:text-sm">
Sign in to sync your notes and preferences across all your devices and enable end-to-end encryption.
</div>
<Button primary label="Create free account" onClick={clickRegister} className="mb-3" />
<div className="text-base lg:text-sm">
Already have an account?{' '}
<button className="cursor-pointer border-0 bg-default p-0 text-info underline" onClick={clickSignIn}>
Sign in
</button>
{c('Info')
.t`Sign in to sync your notes and preferences across all your devices and enable end-to-end encryption.`}
</div>
<Button primary label={c('Action').t`Create free account`} onClick={clickRegister} className="mb-3" />
<div className="text-base lg:text-sm">{c('Info').jt`Already have an account? ${loginLink}`}</div>
</div>
</PreferencesSegment>
</PreferencesGroup>

View File

@@ -9,6 +9,7 @@ import { BlueDotIcon, CircleIcon, DiamondIcon, CreateAccountIllustration } from
import { useCaptcha } from '@/Hooks/useCaptcha'
import { AccountMenuPane } from '../../AccountMenu/AccountMenuPane'
import { isErrorResponse } from '@standardnotes/snjs'
import { c } from 'ttag'
type Props = {
application: WebApplication
@@ -150,19 +151,19 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
className={`min-w-auto md:min-w-90 ${isEmailInvalid ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-email"
type="email"
label="Email"
label={c('Label').t`Email`}
value={email}
onChange={handleEmailChange}
ref={emailInputRef}
disabled={isCreatingAccount}
isInvalid={isEmailInvalid}
/>
{isEmailInvalid ? <div className="mb-4 text-danger">Please provide a valid email.</div> : null}
{isEmailInvalid ? <div className="mb-4 text-danger">{c('Error').t`Please provide a valid email.`}</div> : null}
<FloatingLabelInput
className="min-w-auto mb-4 md:min-w-90"
id="purchase-create-account-password"
type="password"
label="Password"
label={c('Label').t`Password`}
value={password}
onChange={handlePasswordChange}
ref={passwordInputRef}
@@ -172,7 +173,7 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
className={`min-w-auto md:min-w-90 ${isPasswordNotMatching ? 'mb-2' : 'mb-4'}`}
id="create-account-confirm"
type="password"
label="Repeat password"
label={c('Label').t`Repeat password`}
value={confirmPassword}
onChange={handleConfirmPasswordChange}
ref={confirmPasswordInputRef}
@@ -180,7 +181,7 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
isInvalid={isPasswordNotMatching}
/>
{isPasswordNotMatching ? (
<div className="mb-4 text-danger">Passwords don't match. Please try again.</div>
<div className="mb-4 text-danger">{c('Error').t`Passwords don't match. Please try again.`}</div>
) : null}
</div>
</form>
@@ -197,8 +198,10 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
<DiamondIcon className="absolute -right-2 top-0 -z-[1] h-18 w-18 translate-x-1/2" />
<div className="mr-0 lg:mr-12">
<h1 className="mb-2 mt-0 text-2xl font-bold">Create your free account</h1>
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
// translator: Full sentence: "Create your free account"
<h1 className="mb-2 mt-0 text-2xl font-bold">{c('Title').t`Create your free account`}</h1>
// translator: Full sentence: "Create your free account to continue to Standard Notes."
<div className="mb-4 text-sm font-medium">{c('Info').t`to continue to Standard Notes.`}</div>
{captchaURL ? captchaIframe : CreateAccountForm}
<div className="flex flex-col-reverse items-start justify-between md:flex-row md:items-center">
<div className="flex flex-col">
@@ -207,7 +210,8 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
disabled={isCreatingAccount}
className="mb-2 flex cursor-pointer items-start border-0 bg-default p-0 font-medium text-info hover:underline"
>
Sign in instead
// translator: "Instead" here refers to "instead of creating an account"
{c('Action').t`Sign in instead`}
</button>
{!application.isNativeIOS() && (
<button
@@ -215,14 +219,14 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
disabled={isCreatingAccount}
className="flex cursor-pointer items-start border-0 bg-default p-0 font-medium text-info hover:underline"
>
Subscribe without account
{c('Action').t`Subscribe without account`}
</button>
)}
</div>
<Button
className="mb-4 py-2.5 md:mb-0"
primary
label={isCreatingAccount ? 'Creating account...' : 'Create account'}
label={isCreatingAccount ? c('Action').t`Creating account...` : c('Action').t`Create account`}
onClick={handleCreateAccount}
disabled={isCreatingAccount}
/>

View File

@@ -8,6 +8,7 @@ import { isEmailValid } from '@/Utils'
import { BlueDotIcon, CircleIcon, DiamondIcon } from '@standardnotes/icons'
import { isErrorResponse, getCaptchaHeader } from '@standardnotes/snjs'
import { useCaptcha } from '@/Hooks/useCaptcha'
import { c } from 'ttag'
type Props = {
application: WebApplication
@@ -102,7 +103,7 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
if ((err as Error).toString().includes('Invalid email or password')) {
setIsEmailInvalid(true)
setIsPasswordInvalid(true)
setOtherErrorMessage('Invalid email or password.')
setOtherErrorMessage(c('Error').t`Invalid email or password.`)
setPassword('')
} else {
application.alerts.alert(err as string).catch(console.error)
@@ -119,7 +120,7 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
className={`min-w-auto sm:min-w-90 ${isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-email"
type="email"
label="Email"
label={c('Label').t`Email`}
value={email}
onChange={handleEmailChange}
ref={emailInputRef}
@@ -127,13 +128,13 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
isInvalid={isEmailInvalid}
/>
{isEmailInvalid && !otherErrorMessage ? (
<div className="mb-4 text-danger">Please provide a valid email.</div>
<div className="mb-4 text-danger">{c('Error').t`Please provide a valid email.`}</div>
) : null}
<FloatingLabelInput
className={`min-w-auto sm:min-w-90 ${otherErrorMessage ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-password"
type="password"
label="Password"
label={c('Label').t`Password`}
value={password}
onChange={handlePasswordChange}
ref={passwordInputRef}
@@ -145,7 +146,7 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
<Button
className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} mb-5 py-2.5`}
primary
label={isSigningIn ? 'Signing in...' : 'Sign in'}
label={isSigningIn ? c('Action').t`Signing in...` : c('Action').t`Sign in`}
onClick={handleSignIn}
disabled={isSigningIn}
/>
@@ -163,17 +164,18 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
<DiamondIcon className="absolute -right-2 top-0 -z-[1] h-18 w-18 translate-x-1/2" />
<div>
<h1 className="mb-2 mt-0 text-2xl font-bold">Sign in</h1>
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
<h1 className="mb-2 mt-0 text-2xl font-bold">{c('Title').t`Sign in`}</h1>
<div className="mb-4 text-sm font-medium">{c('Info').t`to continue to Standard Notes.`}</div>
{showCaptcha ? captchaIframe : signInForm}
<div className="text-sm font-medium text-passive-1">
Dont have an account yet?{' '}
<a
className={`text-info ${isSigningIn ? 'cursor-not-allowed' : 'cursor-pointer '}`}
onClick={handleCreateAccountInstead}
>
Create account
</a>
{c('Info').jt`Dont have an account yet? ${(
<a
className={`text-info ${isSigningIn ? 'cursor-not-allowed' : 'cursor-pointer '}`}
onClick={handleCreateAccountInstead}
>
{c('Action').t`Create account`}
</a>
)}`}
</div>
</div>
</div>