chore: first ttag batch (#3007) [skip ci]

* chore: set ttag for sign in and sign up strings

* chore: set ttag for account menu and session actions

* chore: set ttag for password change and re-encryption

* chore: set ttag for import and export account data
This commit is contained in:
Antonella Sgarlatta
2026-05-28 14:37:02 -03:00
committed by GitHub
parent 61bf8ca2f6
commit 6c07fab9e9
32 changed files with 309 additions and 224 deletions
@@ -6,6 +6,7 @@ import Icon from '@/Components/Icon/Icon'
import { useApplication } from '../ApplicationProvider'
import ServerPicker from './ServerPicker/ServerPicker'
import { DefaultHost } from '@standardnotes/snjs'
import { c } from 'ttag'
type Props = {
disabled?: boolean
@@ -50,7 +51,7 @@ const AdvancedOptions: FunctionComponent<Props> = ({
if (!identifier) {
if (privateUsername?.length > 0) {
application.alerts.alert('Unable to compute private username.').catch(console.error)
application.alerts.alert(c('Error').t`Unable to compute private username.`).catch(console.error)
}
return
}
@@ -112,7 +113,7 @@ const AdvancedOptions: FunctionComponent<Props> = ({
onClick={toggleShowAdvanced}
>
<div className="flex items-center">
Advanced options
{c('Action').t`Advanced options`}
<Icon type="chevron-down" className="ml-1 text-passive-1" />
</div>
</button>
@@ -124,12 +125,17 @@ const AdvancedOptions: FunctionComponent<Props> = ({
<div className="mb-1 flex items-center justify-between">
<Checkbox
name="private-workspace"
label="Private username mode"
label={c('Option').t`Private username mode`}
checked={isPrivateUsername}
disabled={disabled || isRecoveryCodes}
onChange={handleIsPrivateUsernameChange}
/>
<a href="https://standardnotes.com/help/80" target="_blank" rel="noopener noreferrer" title="Learn more">
<a
href="https://standardnotes.com/help/80"
target="_blank"
rel="noopener noreferrer"
title={c('Action').t`Learn more`}
>
<Icon type="info" className="text-neutral" />
</a>
</div>
@@ -140,7 +146,7 @@ const AdvancedOptions: FunctionComponent<Props> = ({
className={{ container: 'mb-2' }}
left={[<Icon type="account-circle" className="text-neutral" />]}
type="text"
placeholder="Username"
placeholder={c('Label').t`Username`}
value={privateUsername}
onChange={handlePrivateUsernameNameChange}
disabled={disabled || isRecoveryCodes}
@@ -154,7 +160,7 @@ const AdvancedOptions: FunctionComponent<Props> = ({
<div className="mb-1 flex items-center justify-between">
<Checkbox
name="use-strict-signin"
label="Use strict sign-in"
label={c('Option').t`Use strict sign-in`}
checked={isStrictSignin}
disabled={disabled || isRecoveryCodes}
onChange={handleStrictSigninChange}
@@ -163,7 +169,7 @@ const AdvancedOptions: FunctionComponent<Props> = ({
href="https://standardnotes.com/help/security"
target="_blank"
rel="noopener noreferrer"
title="Learn more"
title={c('Action').t`Learn more`}
>
<Icon type="info" className="text-neutral" />
</a>
@@ -174,7 +180,7 @@ const AdvancedOptions: FunctionComponent<Props> = ({
<div className="mb-1 flex items-center justify-between">
<Checkbox
name="recovery-codes"
label="Use recovery code"
label={c('Option').t`Use recovery code`}
checked={isRecoveryCodes}
disabled={disabled}
onChange={handleIsRecoveryCodesChange}
@@ -188,7 +194,7 @@ const AdvancedOptions: FunctionComponent<Props> = ({
className={{ container: 'mb-2' }}
left={[<Icon type="security" className="text-neutral" />]}
type="text"
placeholder="Recovery code"
placeholder={c('Label').t`Recovery code`}
value={recoveryCodes}
onChange={handleRecoveryCodesChange}
disabled={disabled}
@@ -2,6 +2,7 @@ import AlertDialog from '@/Components/AlertDialog/AlertDialog'
import Button from '@/Components/Button/Button'
import Icon from '@/Components/Icon/Icon'
import { FunctionComponent } from 'react'
import { c } from 'ttag'
type Props = {
onClose: () => void
@@ -12,7 +13,7 @@ const ConfirmNoMergeDialog: FunctionComponent<Props> = ({ onClose, onConfirm })
return (
<AlertDialog closeDialog={onClose}>
<div className="flex items-center justify-between text-lg font-bold">
Delete local data?
{c('Title').t`Delete local data?`}
<button className="rounded p-1 font-bold hover:bg-contrast" onClick={onClose}>
<Icon type="close" />
</button>
@@ -20,18 +21,18 @@ const ConfirmNoMergeDialog: FunctionComponent<Props> = ({ onClose, onConfirm })
<div className="sk-panel-row">
<div>
<p className="text-base text-foreground lg:text-sm">
You have chosen not to merge your local data. If you proceed, your local notes and tags will be permanently
deleted and replaced with data from your account. This action cannot be undone.
{c('Info')
.t`You have chosen not to merge your local data. If you proceed, your local notes and tags will be permanently deleted and replaced with data from your account. This action cannot be undone.`}
</p>
<p className="mt-2 text-base font-semibold text-danger lg:text-sm">
Are you sure you want to continue without merging?
{c('Info').t`Are you sure you want to continue without merging?`}
</p>
</div>
</div>
<div className="mt-4 flex justify-end gap-2">
<Button onClick={onClose}>Cancel</Button>
<Button onClick={onClose}>{c('Action').t`Cancel`}</Button>
<Button primary colorStyle="danger" onClick={onConfirm}>
Delete Local Data and Continue
{c('Action').t`Delete Local Data and Continue`}
</Button>
</div>
</AlertDialog>
@@ -122,7 +122,7 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
}
if (password !== confirmPassword) {
setError(STRING_NON_MATCHING_PASSWORDS)
setError(STRING_NON_MATCHING_PASSWORDS())
setConfirmPassword('')
passwordInputRef.current?.focus()
return
@@ -47,7 +47,7 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
}
})
.catch(() => {
application.alerts.alert(STRING_GENERIC_SYNC_ERROR).catch(console.error)
application.alerts.alert(STRING_GENERIC_SYNC_ERROR()).catch(console.error)
})
.finally(() => {
setIsSyncingInProgress(false)
@@ -69,9 +69,9 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
}, [application])
const openEmail = useCallback(() => {
const subject = 'Standard Notes Feedback'
const subject = c('MailtoSubject').t`Standard Notes Feedback`
const body = `App Version: ${application.version}`
const body = c('MailtoBody').t`App Version: ${application.version}`
application.device.openUrl(
`mailto:[email protected]?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`,
@@ -103,7 +103,7 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
return (
<>
<div className="mb-1 mt-1 hidden items-center justify-between px-4 md:flex md:px-3">
<div className="text-lg font-bold lg:text-base">Account</div>
<div className="text-lg font-bold lg:text-base">{c('Title').t`Account`}</div>
<div className="flex cursor-pointer" onClick={closeMenu}>
<Icon type="close" className="text-neutral" />
</div>
@@ -111,7 +111,7 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
{user ? (
<>
<div className="mb-3 px-4 text-lg text-foreground md:px-3 lg:text-sm">
<div>You're signed in as:</div>
<div>{c('Info').t`You're signed in as:`}</div>
<div className="wrap my-0.5 font-bold">{user.email}</div>
<span className="text-neutral">{application.getHost.execute().getValue()}</span>
</div>
@@ -119,13 +119,13 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
{isSyncingInProgress ? (
<div className="flex items-center font-semibold text-info">
<Spinner className="mr-2 h-5 w-5" />
Syncing...
{c('Status').t`Syncing...`}
</div>
) : (
<div className="flex items-start">
<Icon type="check-circle" className={`mr-2 text-success ${MenuItemIconSize}`} />
<div>
<div className="font-semibold text-success">Last synced:</div>
<div className="font-semibold text-success">{c('Label').t`Last synced:`}</div>
<div className="text-text">{lastSyncDate}</div>
</div>
</div>
@@ -161,7 +161,7 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
{user ? (
<MenuItem onClick={openPreferences}>
<Icon type="user" className={iconClassName} />
Account settings
{c('Action').t`Account settings`}
</MenuItem>
) : (
<>
@@ -182,18 +182,18 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
}}
>
<Icon type="archive" className={iconClassName} />
Import
{c('Action').t`Import`}
</MenuItem>
{application.isNativeMobileWeb() && (
<MenuItem onClick={openEmail}>
<Icon type="email-filled" className={iconClassName} />
Email us
{c('Action').t`Email us`}
</MenuItem>
)}
<MenuItem className="justify-between" onClick={openHelp}>
<div className="flex items-center">
<Icon type="help" className={iconClassName} />
Help &amp; feedback
{c('Action').t`Help & feedback`}
</div>
<span className="text-neutral">v{application.version}</span>
</MenuItem>
@@ -205,7 +205,7 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
}}
>
<Icon type="keyboard" className={iconClassName} />
Keyboard shortcuts
{c('Action').t`Keyboard shortcuts`}
{keyboardShortcutsHelpShortcut && (
<KeyboardShortcutIndicator shortcut={keyboardShortcutsHelpShortcut} className="ml-auto" />
)}
@@ -216,7 +216,7 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
}}
>
<Icon type="info" className={iconClassName} />
Command palette
{c('Action').t`Command palette`}
{commandPaletteShortcut && (
<KeyboardShortcutIndicator shortcut={commandPaletteShortcut} className="ml-auto" />
)}
@@ -228,7 +228,7 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
<MenuSection>
<MenuItem onClick={signOut}>
<Icon type="signOut" className={iconClassName} />
Sign out workspace
{c('Action').t`Sign out workspace`}
</MenuItem>
</MenuSection>
) : null}
@@ -1,6 +1,7 @@
import Icon from '@/Components/Icon/Icon'
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
import { ChangeEventHandler, FunctionComponent } from 'react'
import { c } from 'ttag'
type Props = {
checked: boolean
@@ -21,9 +22,10 @@ const MergeLocalDataCheckbox: FunctionComponent<Props> = ({ checked, onChange, d
onChange={onChange}
disabled={disabled}
/>
<span className="text-danger">Merge local data ({notesAndTagsCount} notes and tags)</span>
<span className="text-danger">{c('Option').t`Merge local data (${notesAndTagsCount} notes and tags)`}</span>
<StyledTooltip
label="If unchecked, your local notes and tags will be permanently deleted and replaced with data from your account."
label={c('Info')
.t`If unchecked, your local notes and tags will be permanently deleted and replaced with data from your account.`}
showOnMobile
className="!z-modal !max-w-[30ch] whitespace-normal"
>
@@ -6,6 +6,7 @@ import { useApplication } from '@/Components/ApplicationProvider'
import { isDesktopApplication } from '@/Utils'
import RadioButtonGroup from '@/Components/RadioButtonGroup/RadioButtonGroup'
import { DefaultHost } from '@standardnotes/snjs'
import { c } from 'ttag'
type Props = {
className?: string
@@ -49,7 +50,7 @@ const ServerPicker = ({ className }: Props) => {
} else if (type === 'home server') {
if (!application.homeServer) {
application.alerts
.alert('Home server is not running. Please open the prefences and home server tab to start it.')
.alert(c('Error').t`Home server is not running. Please open the prefences and home server tab to start it.`)
.catch(console.error)
return
@@ -58,7 +59,7 @@ const ServerPicker = ({ className }: Props) => {
const homeServerUrl = await application.homeServer.getHomeServerUrl()
if (!homeServerUrl) {
application.alerts
.alert('Home server is not running. Please open the prefences and home server tab to start it.')
.alert(c('Error').t`Home server is not running. Please open the prefences and home server tab to start it.`)
.catch(console.error)
return
@@ -71,9 +72,9 @@ const ServerPicker = ({ className }: Props) => {
const options = useMemo(
() =>
[
{ label: 'Default', value: 'standard' },
{ label: 'Custom', value: 'custom' },
].concat(isDesktopApplication() ? [{ label: 'Home Server', value: 'home server' }] : []) as {
{ label: c('Option').t`Default`, value: 'standard' },
{ label: c('Option').t`Custom`, value: 'custom' },
].concat(isDesktopApplication() ? [{ label: c('Option').t`Home Server`, value: 'home server' }] : []) as {
label: string
value: ServerType
}[],
@@ -82,7 +83,7 @@ const ServerPicker = ({ className }: Props) => {
return (
<div className={`flex h-full flex-grow flex-col px-3 pb-1.5 ${className}`}>
<div className="mb-2 flex font-bold">Sync Server</div>
<div className="mb-2 flex font-bold">{c('Label').t`Sync Server`}</div>
<RadioButtonGroup value={currentType} items={options} onChange={selectTab} />
{currentType === 'custom' && (
<DecoratedInput
@@ -1,6 +1,7 @@
import { observer } from 'mobx-react-lite'
import { User as UserType } from '@standardnotes/snjs'
import { useApplication } from '../ApplicationProvider'
import { c } from 'ttag'
const User = () => {
const application = useApplication()
@@ -12,9 +13,10 @@ const User = () => {
<div className="sk-panel-section">
{application.syncStatusController.errorMessage && (
<div className="sk-notification danger">
<div className="sk-notification-title">Sync Unreachable</div>
<div className="sk-notification-title">{c('Title').t`Sync Unreachable`}</div>
<div className="sk-notification-text">
Hmm...we can't seem to sync your account. The reason: {application.syncStatusController.errorMessage}
{c('Error')
.t`Hmm...we can't seem to sync your account. The reason: ${application.syncStatusController.errorMessage}`}
</div>
<a
className="sk-a info-contrast sk-bold sk-panel-row"
@@ -22,7 +24,7 @@ const User = () => {
rel="noopener"
target="_blank"
>
Need help?
{c('Action').t`Need help?`}
</a>
</div>
)}
@@ -8,6 +8,7 @@ import MenuItem from '@/Components/Menu/MenuItem'
import WorkspaceMenuItem from './WorkspaceMenuItem'
import { useApplication } from '@/Components/ApplicationProvider'
import MenuSection from '@/Components/Menu/MenuSection'
import { c } from 'ttag'
type Props = {
mainApplicationGroup: WebApplicationGroup
@@ -42,9 +43,9 @@ const WorkspaceSwitcherMenu: FunctionComponent<Props> = ({
const signoutAll = useCallback(async () => {
const confirmed = await application.alerts.confirm(
'Are you sure you want to sign out of all workspaces on this device?',
c('Info').t`Are you sure you want to sign out of all workspaces on this device?`,
undefined,
'Sign out all',
c('Action').t`Sign out all`,
ButtonType.Danger,
)
if (!confirmed) {
@@ -86,12 +87,12 @@ const WorkspaceSwitcherMenu: FunctionComponent<Props> = ({
<MenuSection>
<MenuItem onClick={addAnotherWorkspace}>
<Icon type="user-add" className="mr-2 text-neutral" />
Add another workspace
{c('Action').t`Add another workspace`}
</MenuItem>
{!hideWorkspaceOptions && (
<MenuItem onClick={signoutAll}>
<Icon type="signOut" className="mr-2 text-neutral" />
Sign out all workspaces
{c('Action').t`Sign out all workspaces`}
</MenuItem>
)}
</MenuSection>
@@ -7,6 +7,7 @@ import WorkspaceSwitcherMenu from './WorkspaceSwitcherMenu'
import MenuItem from '@/Components/Menu/MenuItem'
import Popover from '@/Components/Popover/Popover'
import { MenuItemIconSize } from '@/Constants/TailwindClassNames'
import { c } from 'ttag'
type Props = {
mainApplicationGroup: WebApplicationGroup
@@ -25,12 +26,12 @@ const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGrou
<MenuItem tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} ref={buttonRef} onClick={toggleMenu} className="justify-between">
<div className="flex items-center">
<Icon type="user-switch" className={`mr-2 text-neutral ${MenuItemIconSize}`} />
Switch workspace
{c('Action').t`Switch workspace`}
</div>
<Icon type="chevron-right" className={`text-neutral ${MenuItemIconSize}`} />
</MenuItem>
<Popover
title="Switch workspace"
title={c('Action').t`Switch workspace`}
align="end"
anchorElement={buttonRef}
className="pb-2"
@@ -1,5 +1,6 @@
import { observer } from 'mobx-react-lite'
import { useCallback, useRef } from 'react'
import { c } from 'ttag'
import { STRING_DELETE_ACCOUNT_CONFIRMATION } from '@/Constants/Strings'
import Button from '@/Components/Button/Button'
import { WebApplication } from '@/Application/WebApplication'
@@ -25,22 +26,22 @@ const ConfirmDeleteAccountModal = ({ application }: Props) => {
return (
<AlertDialog closeDialog={closeDialog}>
<div className="flex items-center justify-between text-lg font-bold">
Delete account?
{c('Title').t`Delete account?`}
<button className="rounded p-1 font-bold hover:bg-contrast" onClick={closeDialog}>
<Icon type="close" />
</button>
</div>
<div className="sk-panel-row">
<div>
<p className="text-base text-foreground lg:text-sm">{STRING_DELETE_ACCOUNT_CONFIRMATION}</p>
<p className="text-base text-foreground lg:text-sm">{STRING_DELETE_ACCOUNT_CONFIRMATION()}</p>
</div>
</div>
<div className="mt-4 flex justify-end gap-2">
<Button ref={cancelRef} onClick={closeDialog}>
Cancel
{c('Action').t`Cancel`}
</Button>
<Button primary colorStyle="danger" onClick={confirm}>
Delete my account for good
{c('Action').t`Delete my account for good`}
</Button>
</div>
</AlertDialog>
@@ -8,6 +8,7 @@ import Button from '@/Components/Button/Button'
import Icon from '../Icon/Icon'
import AlertDialog from '../AlertDialog/AlertDialog'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { c } from 'ttag'
type Props = {
application: WebApplication
@@ -37,22 +38,21 @@ const ConfirmSignoutModal: FunctionComponent<Props> = ({ application, applicatio
return (
<AlertDialog closeDialog={closeDialog}>
<div className="flex items-center justify-between text-lg font-bold">
Sign out workspace?
{c('Title').t`Sign out workspace?`}
<button className="rounded p-1 font-bold hover:bg-contrast" onClick={closeDialog}>
<Icon type="close" />
</button>
</div>
<div className="sk-panel-row">
<div>
<p className="text-base text-foreground lg:text-sm">{STRING_SIGN_OUT_CONFIRMATION}</p>
<p className="text-base text-foreground lg:text-sm">{STRING_SIGN_OUT_CONFIRMATION()}</p>
{showWorkspaceWarning && (
<>
<br />
<p className="text-base text-foreground lg:text-sm">
<strong>Note: </strong>
Because you have other workspaces signed in, this sign out may leave logs and other metadata of your
session on this device. For a more robust sign out that performs a hard clear of all app-related data,
use the <i>Sign out all workspaces</i> option under <i>Switch workspace</i>.
<strong>{c('Label').t`Note:`} </strong>
{c('Info')
.t`Because you have other workspaces signed in, this sign out may leave logs and other metadata of your session on this device. For a more robust sign out that performs a hard clear of all app-related data, use the "Sign out all workspaces" option under "Switch workspace".`}
</p>
</>
)}
@@ -66,7 +66,8 @@ const ConfirmSignoutModal: FunctionComponent<Props> = ({ application, applicatio
<div className="sk-panel-row"></div>
<div>
<p className="text-base text-foreground lg:text-sm">
Local backups are enabled for this workspace. Review your backup files manually to decide what to keep.
{c('Info')
.t`Local backups are enabled for this workspace. Review your backup files manually to decide what to keep.`}
</p>
<button
className="sk-a mt-2 cursor-pointer rounded p-0 capitalize lg:text-sm"
@@ -74,7 +75,7 @@ const ConfirmSignoutModal: FunctionComponent<Props> = ({ application, applicatio
void application.fileBackups?.openAllDirectoriesContainingBackupFiles()
}}
>
View backup files
{c('Action').t`View backup files`}
</button>
</div>
</div>
@@ -83,10 +84,10 @@ const ConfirmSignoutModal: FunctionComponent<Props> = ({ application, applicatio
<div className="mt-4 flex justify-end gap-2">
<Button ref={cancelRef} onClick={closeDialog}>
Cancel
{c('Action').t`Cancel`}
</Button>
<Button primary colorStyle="danger" onClick={confirm}>
{application.hasAccount() ? 'Sign Out' : 'Delete Workspace'}
{application.hasAccount() ? c('Action').t`Sign Out` : c('Action').t`Delete Workspace`}
</Button>
</div>
</AlertDialog>
@@ -287,12 +287,12 @@ class Footer extends AbstractComponent<Props, State> {
securityUpdateClickHandler = async () => {
if (
await confirmDialog({
title: STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE,
text: STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT,
confirmButtonText: STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON,
title: STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE(),
text: STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT(),
confirmButtonText: STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON(),
})
) {
preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_UPGRADE, async () => {
preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_UPGRADE(), async () => {
await this.application.upgradeProtocolVersion()
}).catch(console.error)
}
@@ -13,6 +13,7 @@ import ItemSelectionDropdown from '../ItemSelectionDropdown/ItemSelectionDropdow
import { ContentType, SNTag } from '@standardnotes/snjs'
import Button from '../Button/Button'
import { ClassicFileReader } from '@standardnotes/filepicker'
import { c } from 'ttag'
const ImportModal = ({ importModalController }: { importModalController: ImportModalController }) => {
const application = useApplication()
@@ -41,7 +42,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
const modalActions: ModalAction[] = useMemo(
() => [
{
label: 'Import',
label: c('Action').t`Import`,
type: 'primary',
onClick: parseAndImport,
hidden: !isReadyToImport,
@@ -49,7 +50,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
disabled: !isReadyToImport || (addImportsToTag && !shouldCreateTag && !existingTagForImports),
},
{
label: importSuccessOrError ? 'Close' : 'Cancel',
label: importSuccessOrError ? c('Action').t`Close` : c('Action').t`Cancel`,
type: 'cancel',
onClick: close,
mobileSlot: 'left',
@@ -77,7 +78,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
return (
<ModalOverlay isOpen={isVisible} close={close}>
<Modal title="Import" close={close} actions={modalActions} className="flex flex-col">
<Modal title={c('Title').t`Import`} close={close} actions={modalActions} className="flex flex-col">
<div className="min-h-0 flex-grow overflow-y-auto px-4 py-4">
{!files.length && <ImportModalInitialPage setFiles={setFiles} selectFiles={selectFiles} />}
{files.length > 0 && (
@@ -101,7 +102,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
}}
small
>
Add files
{c('Action').t`Add files`}
</Button>
)}
</>
@@ -110,7 +111,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
{files.length > 0 && (
<div className="flex flex-col gap-3 border-t border-border px-4 py-4 md:gap-2 md:py-3">
<Switch className="flex items-center gap-2" checked={addImportsToTag} onChange={setAddImportsToTag}>
<span className="text-sm">Add all imported notes to tag</span>
<span className="text-sm">{c('Label').t`Add all imported notes to tag`}</span>
</Switch>
{addImportsToTag && (
<>
@@ -124,7 +125,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
setShouldCreateTag(true)
}}
/>
Create new tag
{c('Label').t`Create new tag`}
</label>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
@@ -138,7 +139,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
setShouldCreateTag(false)
}}
/>
Add to existing tag
{c('Label').t`Add to existing tag`}
</label>
{existingTagForImports && (
<LinkedItemBubble
@@ -156,7 +157,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
<div className="ml-8 md:ml-6">
<ItemSelectionDropdown
onSelection={(tag) => setExistingTagForImports(tag as SNTag)}
placeholder="Select tag to add imported notes to..."
placeholder={c('Placeholder').t`Select tag to add imported notes to...`}
contentTypes={[ContentType.TYPES.Tag]}
/>
</div>
@@ -1,10 +1,11 @@
import { ImportModalController, ImportModalFile } from '@/Components/ImportModal/ImportModalController'
import { classNames, ContentType, pluralize } from '@standardnotes/snjs'
import { classNames, ContentType } from '@standardnotes/snjs'
import { ConversionResult, Importer } from '@standardnotes/ui-services'
import { observer } from 'mobx-react-lite'
import { useCallback, useEffect, useState } from 'react'
import Icon from '../Icon/Icon'
import { Disclosure, DisclosureContent, DisclosureProvider } from '@ariakit/react'
import { c, msgid, ngettext } from 'ttag'
const NoteImportTypeColors: Record<string, string> = {
evernote: 'bg-[#14cc45] text-[#000]',
@@ -49,12 +50,17 @@ const countSuccessfulItemsByGroup = (successful: ConversionResult['successful'])
}
const ImportErroredAccordion = ({ errored }: { errored: ConversionResult['errored'] }) => {
const count = errored.length
return (
<DisclosureProvider>
<Disclosure>
<div className="flex items-center gap-1">
<Icon type="warning" className="flex-shrink-0 text-danger" size="small" />
Could not import {errored.length} {pluralize(errored.length, 'item', 'items')} (click for details)
{ngettext(
msgid`Could not import ${count} item (click for details)`,
`Could not import ${count} items (click for details)`,
count,
)}
</div>
</Disclosure>
<DisclosureContent className="w-full overflow-hidden pl-5">
@@ -77,9 +83,9 @@ const ImportFinishedStatus = ({ file }: { file: ImportModalFile }) => {
const { notes, tags, files } = countSuccessfulItemsByGroup(file.successful)
const notesStatus = notes > 0 ? `${notes} ${pluralize(notes, 'note', 'notes')}` : ''
const tagsStatus = tags > 0 ? `${tags} ${pluralize(tags, 'tag', 'tags')}` : ''
const filesStatus = files > 0 ? `${files} ${pluralize(files, 'file', 'files')}` : ''
const notesStatus = notes > 0 ? ngettext(msgid`${notes} note`, `${notes} notes`, notes) : ''
const tagsStatus = tags > 0 ? ngettext(msgid`${tags} tag`, `${tags} tags`, tags) : ''
const filesStatus = files > 0 ? ngettext(msgid`${files} file`, `${files} files`, files) : ''
const status = [notesStatus, tagsStatus, filesStatus].filter(Boolean).join(', ')
return (
@@ -87,7 +93,7 @@ const ImportFinishedStatus = ({ file }: { file: ImportModalFile }) => {
{file.successful.length > 0 && (
<div className="flex items-center gap-1">
<Icon type="check-circle-filled" className="flex-shrink-0 text-success" size="small" />
<span>{status} imported</span>
<span>{c('Info').t`${status} imported`}</span>
</div>
)}
{file.errored.length > 0 && <ImportErroredAccordion errored={file.errored} />}
@@ -156,14 +162,16 @@ const ImportModalFileItem = ({
<div className="flex w-full flex-col overflow-hidden">
<div>{file.file.name}</div>
{isDetectingService ? (
<div className="text-xs opacity-75">Detecting service...</div>
<div className="text-xs opacity-75">{c('Status').t`Detecting service...`}</div>
) : (
<div className={classNames(file.status !== 'finished' && 'line-clamp-3', 'w-full text-xs opacity-75')}>
{file.status === 'pending' && file.service && 'Ready to import'}
{file.status === 'pending' && !file.service && 'Could not auto-detect service. Please select manually.'}
{file.status === 'parsing' && 'Parsing...'}
{file.status === 'importing' && 'Importing...'}
{file.status === 'uploading-files' && 'Uploading and embedding files...'}
{file.status === 'pending' && file.service && c('Status').t`Ready to import`}
{file.status === 'pending' &&
!file.service &&
c('Status').t`Could not auto-detect service. Please select manually.`}
{file.status === 'parsing' && c('Status').t`Parsing...`}
{file.status === 'importing' && c('Status').t`Importing...`}
{file.status === 'uploading-files' && c('Status').t`Uploading and embedding files...`}
{file.status === 'error' && file.error.message}
<ImportFinishedStatus file={file} />
</div>
@@ -197,7 +205,7 @@ const ImportModalFileItem = ({
<option value="super">Super</option>
</select>
<button
aria-label="Choose service"
aria-label={c('AriaLabel').t`Choose service`}
type="submit"
className="rounded border border-border bg-default p-1.5 hover:bg-contrast"
>
@@ -205,7 +213,7 @@ const ImportModalFileItem = ({
</button>
</form>
<button
aria-label="Cancel"
aria-label={c('AriaLabel').t`Cancel`}
className="ml-2 rounded border border-border bg-default p-1.5 hover:bg-contrast"
onClick={() => {
setChangingService(false)
@@ -216,7 +224,7 @@ const ImportModalFileItem = ({
</>
) : (
<button
aria-label="Change service"
aria-label={c('AriaLabel').t`Change service`}
className="rounded border border-border bg-default p-1.5 hover:bg-contrast"
onClick={() => {
setChangingService(true)
@@ -226,7 +234,7 @@ const ImportModalFileItem = ({
</button>
)}
<button
aria-label="Remove"
aria-label={c('AriaLabel').t`Remove`}
className="ml-2 rounded border border-border bg-default p-1.5 hover:bg-contrast"
onClick={() => {
removeFile(file.id)
@@ -5,6 +5,7 @@ import Icon from '../Icon/Icon'
import { useApplication } from '../ApplicationProvider'
import { FeatureName } from '@/Controllers/FeatureName'
import { NativeFeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
import { c } from 'ttag'
type Props = {
setFiles: ImportModalController['setFiles']
@@ -27,10 +28,10 @@ const ImportModalInitialPage = ({ setFiles, selectFiles }: Props) => {
setFiles(files)
}}
>
<div className="text-lg font-semibold">Drag and drop files to auto-detect and import</div>
<div className="text-sm">Or click to open file picker</div>
<div className="text-lg font-semibold">{c('Info').t`Drag and drop files to auto-detect and import`}</div>
<div className="text-sm">{c('Info').t`Or click to open file picker`}</div>
</button>
<div className="my-4 w-full text-center">or import from:</div>
<div className="my-4 w-full text-center">{c('Info').t`or import from:`}</div>
<div className="flex flex-wrap items-center justify-center gap-4">
<Button className="flex items-center !py-2" onClick={() => selectFiles('evernote')}>
<Icon type="evernote" className="mr-2 text-[#14cc45]" />
@@ -50,7 +51,7 @@ const ImportModalInitialPage = ({ setFiles, selectFiles }: Props) => {
</Button>
<Button className="flex items-center !py-2" onClick={() => selectFiles('plaintext')}>
<Icon type="plain-text" className="mr-2 text-info" />
Plaintext / Markdown
{c('ImportSource').t`Plaintext / Markdown`}
</Button>
<Button className="flex items-center !py-2" onClick={() => selectFiles('html')}>
<Icon type="rich-text" className="mr-2 text-accessory-tint-2" />
@@ -1,3 +1,4 @@
import { c } from 'ttag'
import { CheckmarkCircle } from '../UIElements/CheckmarkCircle'
export const FinishStep = () => {
@@ -8,8 +9,11 @@ export const FinishStep = () => {
<CheckmarkCircle />
</div>
<div className="flex flex-col">
<div className="text-base font-bold">Your password has been successfully changed.</div>
<p>Ensure you are running the latest version of Standard Notes on all platforms for maximum compatibility.</p>
<div className="text-base font-bold">{c('Info').t`Your password has been successfully changed.`}</div>
<p>
{c('Info')
.t`Ensure you are running the latest version of Standard Notes on all platforms for maximum compatibility.`}
</p>
</div>
</div>
</div>
@@ -1,4 +1,5 @@
import { useState } from 'react'
import { c } from 'ttag'
import DecoratedPasswordInput from '../Input/DecoratedPasswordInput'
export const PasswordStep = ({
@@ -33,7 +34,7 @@ export const PasswordStep = ({
<div className="flex flex-col pb-1.5">
<form>
<label htmlFor="password-wiz-current-password" className="mb-1 block">
Current Password
{c('Label').t`Current Password`}
</label>
<DecoratedPasswordInput
@@ -47,7 +48,7 @@ export const PasswordStep = ({
<div className="min-h-2" />
<label htmlFor="password-wiz-new-password" className="mb-1 block">
New Password
{c('Label').t`New Password`}
</label>
<DecoratedPasswordInput
@@ -60,7 +61,7 @@ export const PasswordStep = ({
<div className="min-h-2" />
<label htmlFor="password-wiz-confirm-new-password" className="mb-1 block">
Confirm New Password
{c('Label').t`Confirm New Password`}
</label>
<DecoratedPasswordInput
@@ -6,14 +6,21 @@ import Spinner from '../Spinner/Spinner'
import { PasswordStep } from './PasswordStep'
import { FinishStep } from './FinishStep'
import { PreprocessingStep } from './PreprocessingStep'
import { c } from 'ttag'
interface Props {
application: WebApplication
dismissModal: () => void
}
enum ContinueTitle {
Default,
Generating,
Finish,
}
type State = {
continueTitle: string
continueTitle: ContinueTitle
formData: FormData
isContinuing?: boolean
lockContinue?: boolean
@@ -22,10 +29,6 @@ type State = {
step: Steps
}
const DEFAULT_CONTINUE_TITLE = 'Continue'
const GENERATING_CONTINUE_TITLE = 'Generating Keys...'
const FINISH_CONTINUE_TITLE = 'Finish'
enum Steps {
PreprocessingStep = 'preprocessing-step',
PasswordStep = 'password-step',
@@ -46,7 +49,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
const baseState = {
formData: {},
continueTitle: DEFAULT_CONTINUE_TITLE,
continueTitle: ContinueTitle.Default,
}
if (props.application.featuresController.isVaultsEnabled()) {
@@ -82,7 +85,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
resetContinueState() {
this.setState({
showSpinner: false,
continueTitle: DEFAULT_CONTINUE_TITLE,
continueTitle: ContinueTitle.Default,
isContinuing: false,
})
}
@@ -109,7 +112,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
this.setState({
isContinuing: true,
showSpinner: true,
continueTitle: GENERATING_CONTINUE_TITLE,
continueTitle: ContinueTitle.Generating,
})
const valid = await this.validateCurrentPassword()
@@ -127,7 +130,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
this.setState({
isContinuing: false,
showSpinner: false,
continueTitle: FINISH_CONTINUE_TITLE,
continueTitle: ContinueTitle.Finish,
step: Steps.FinishStep,
})
}
@@ -136,16 +139,18 @@ class PasswordWizard extends AbstractComponent<Props, State> {
const currentPassword = this.state.formData.currentPassword
const newPass = this.state.formData.newPassword
if (!currentPassword || currentPassword.length === 0) {
this.application.alerts.alert('Please enter your current password.').catch(console.error)
this.application.alerts.alert(c('Error').t`Please enter your current password.`).catch(console.error)
return false
}
if (!newPass || newPass.length === 0) {
this.application.alerts.alert('Please enter a new password.').catch(console.error)
this.application.alerts.alert(c('Error').t`Please enter a new password.`).catch(console.error)
return false
}
if (newPass !== this.state.formData.newPasswordConfirmation) {
this.application.alerts.alert('Your new password does not match its confirmation.').catch(console.error)
this.application.alerts
.alert(c('Error').t`Your new password does not match its confirmation.`)
.catch(console.error)
this.setFormDataState({
status: undefined,
}).catch(console.error)
@@ -154,7 +159,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
if (!this.application.sessions.getUser()?.email) {
this.application.alerts
.alert("We don't have your email stored. Please sign out then log back in to fix this issue.")
.alert(c('Error').t`We don't have your email stored. Please sign out then log back in to fix this issue.`)
.catch(console.error)
this.setFormDataState({
status: undefined,
@@ -165,7 +170,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
const success = await this.application.validateAccountPassword(this.state.formData.currentPassword as string)
if (!success) {
this.application.alerts
.alert('The current password you entered is not correct. Please try again.')
.alert(c('Error').t`The current password you entered is not correct. Please try again.`)
.catch(console.error)
}
return success
@@ -180,7 +185,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
})
await this.setFormDataState({
status: 'Processing encryption keys…',
status: c('Status').t`Processing encryption keys…`,
})
const newPassword = this.state.formData.newPassword
@@ -197,13 +202,13 @@ class PasswordWizard extends AbstractComponent<Props, State> {
if (!success) {
this.setFormDataState({
status: 'Unable to process your password. Please try again.',
status: c('Status').t`Unable to process your password. Please try again.`,
}).catch(console.error)
} else {
this.setState({
formData: {
...this.state.formData,
status: 'Successfully changed password.',
status: c('Status').t`Successfully changed password.`,
},
})
}
@@ -212,7 +217,9 @@ class PasswordWizard extends AbstractComponent<Props, State> {
dismiss = () => {
if (this.state.processing) {
this.application.alerts.alert('Cannot close window until pending tasks are complete.').catch(console.error)
this.application.alerts
.alert(c('Error').t`Cannot close window until pending tasks are complete.`)
.catch(console.error)
} else {
this.props.dismissModal()
}
@@ -266,25 +273,37 @@ class PasswordWizard extends AbstractComponent<Props, State> {
}
}
continueLabel(): string {
switch (this.state.continueTitle) {
case ContinueTitle.Generating:
return c('Action').t`Generating Keys...`
case ContinueTitle.Finish:
return c('Action').t`Finish`
case ContinueTitle.Default:
default:
return c('Action').t`Continue`
}
}
override render() {
return (
<div className="sn-component h-full w-full md:h-auto md:w-auto" id="password-wizard">
<Modal
title={'Change Password'}
title={c('Title').t`Change Password`}
close={this.dismiss}
actions={[
{
label: 'Cancel',
label: c('Action').t`Cancel`,
onClick: this.dismiss,
type: 'cancel',
mobileSlot: 'left',
},
{
label:
this.state.continueTitle === GENERATING_CONTINUE_TITLE && isMobileScreen() ? (
this.state.continueTitle === ContinueTitle.Generating && isMobileScreen() ? (
<Spinner className="h-4 w-4" />
) : (
this.state.continueTitle
this.continueLabel()
),
onClick: this.nextStep,
type: 'primary',
@@ -1,6 +1,7 @@
import Spinner from '../Spinner/Spinner'
import { useApplication } from '../ApplicationProvider'
import { useCallback, useEffect, useState } from 'react'
import { c } from 'ttag'
export const PreprocessingStep = ({
onContinue,
@@ -76,7 +77,7 @@ export const PreprocessingStep = ({
return (
<div className="flex flex-row items-center gap-3">
<Spinner className="h-3 w-3" />
<p className="">Checking for data conflicts...</p>
<p className="">{c('Status').t`Checking for data conflicts...`}</p>
</div>
)
}
@@ -88,9 +89,8 @@ export const PreprocessingStep = ({
return (
<div className="flex flex-col">
<p>
You have pending vault invites. Changing your password will delete these invites. It is recommended you accept
or decline these invites before changing your password. If you choose to continue, these invites will be
deleted.
{c('Info')
.t`You have pending vault invites. Changing your password will delete these invites. It is recommended you accept or decline these invites before changing your password. If you choose to continue, these invites will be deleted.`}
</p>
</div>
)
@@ -1,4 +1,5 @@
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
import { c } from 'ttag'
import { WebApplication } from '@/Application/WebApplication'
import { useBeforeUnload } from '@/Hooks/useBeforeUnload'
import ChangeEmailForm from './ChangeEmailForm'
@@ -6,9 +7,9 @@ import ChangeEmailSuccess from './ChangeEmailSuccess'
import Modal, { ModalAction } from '@/Components/Modal/Modal'
enum SubmitButtonTitles {
Default = 'Continue',
GeneratingKeys = 'Generating Keys...',
Finish = 'Finish',
Default,
GeneratingKeys,
Finish,
}
enum Steps {
@@ -35,7 +36,7 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
const validateCurrentPassword = useCallback(async () => {
if (!currentPassword || currentPassword.length === 0) {
applicationAlertService.alert('Please enter your current password.').catch(console.error)
applicationAlertService.alert(c('Error').t`Please enter your current password.`).catch(console.error)
return false
}
@@ -43,7 +44,7 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
const success = await application.validateAccountPassword(currentPassword)
if (!success) {
applicationAlertService
.alert('The current password you entered is not correct. Please try again.')
.alert(c('Error').t`The current password you entered is not correct. Please try again.`)
.catch(console.error)
return false
@@ -73,7 +74,9 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
const dismiss = useCallback(() => {
if (lockContinue) {
applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
applicationAlertService
.alert(c('Error').t`Cannot close window until pending tasks are complete.`)
.catch(console.error)
} else {
onCloseDialog()
}
@@ -115,32 +118,46 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
const handleDialogClose = useCallback(() => {
if (lockContinue) {
applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
applicationAlertService
.alert(c('Error').t`Cannot close window until pending tasks are complete.`)
.catch(console.error)
} else {
onCloseDialog()
}
}, [applicationAlertService, lockContinue, onCloseDialog])
const submitButtonLabel = useMemo(() => {
switch (submitButtonTitle) {
case SubmitButtonTitles.GeneratingKeys:
return c('Action').t`Generating Keys...`
case SubmitButtonTitles.Finish:
return c('Action').t`Finish`
case SubmitButtonTitles.Default:
default:
return c('Action').t`Continue`
}
}, [submitButtonTitle])
const modalActions = useMemo(
(): ModalAction[] => [
{
label: 'Cancel',
label: c('Action').t`Cancel`,
onClick: handleDialogClose,
type: 'cancel',
mobileSlot: 'left',
},
{
label: submitButtonTitle,
label: submitButtonLabel,
onClick: handleSubmit,
type: 'primary',
mobileSlot: 'right',
},
],
[handleDialogClose, handleSubmit, submitButtonTitle],
[handleDialogClose, handleSubmit, submitButtonLabel],
)
return (
<Modal title="Change Email" close={handleDialogClose} actions={modalActions}>
<Modal title={c('Title').t`Change Email`} close={handleDialogClose} actions={modalActions}>
<div className="px-4.5 py-4">
{currentStep === Steps.InitialStep && (
<ChangeEmailForm setNewEmail={setNewEmail} setCurrentPassword={setCurrentPassword} />
@@ -1,6 +1,7 @@
import DecoratedInput from '@/Components/Input/DecoratedInput'
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
import { Dispatch, SetStateAction, FunctionComponent } from 'react'
import { c } from 'ttag'
type Props = {
setNewEmail: Dispatch<SetStateAction<string>>
@@ -14,7 +15,7 @@ const ChangeEmailForm: FunctionComponent<Props> = ({ setNewEmail, setCurrentPass
<div className="flex w-full flex-col">
<div className="mb-3">
<label className={labelClassName} htmlFor="change-email-email-input">
New Email
{c('Label').t`New Email`}
</label>
<DecoratedInput
type="email"
@@ -26,7 +27,7 @@ const ChangeEmailForm: FunctionComponent<Props> = ({ setNewEmail, setCurrentPass
</div>
<div className="mb-2">
<label className={labelClassName} htmlFor="change-email-password-input">
Current Password
{c('Label').t`Current Password`}
</label>
<DecoratedPasswordInput
id="change-email-password-input"
@@ -1,12 +1,13 @@
import { FunctionComponent } from 'react'
import { c } from 'ttag'
const ChangeEmailSuccess: FunctionComponent = () => {
return (
<div>
<div className={'mb-2 font-bold text-info'}>Your email has been successfully changed.</div>
<div className={'mb-2 font-bold text-info'}>{c('Info').t`Your email has been successfully changed.`}</div>
<p>
Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum
compatibility.
{c('Info')
.t`Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum compatibility.`}
</p>
</div>
)
@@ -5,6 +5,7 @@ import { Title, Text } from '../../PreferencesComponents/Content'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import { useApplication } from '@/Components/ApplicationProvider'
import { c } from 'ttag'
const ClearSessionDataView: FunctionComponent = () => {
const application = useApplication()
@@ -12,11 +13,12 @@ const ClearSessionDataView: FunctionComponent = () => {
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>Clear workspace</Title>
<Text className="mb-3">Remove all data related to the current workspace from the application.</Text>
<Title>{c('Title').t`Clear workspace`}</Title>
<Text className="mb-3">{c('Info')
.t`Remove all data related to the current workspace from the application.`}</Text>
<Button
colorStyle="danger"
label="Clear workspace"
label={c('Action').t`Clear workspace`}
onClick={() => {
application.accountMenuController.setSigningOut(true)
}}
@@ -10,6 +10,7 @@ import PasswordWizard from '@/Components/PasswordWizard/PasswordWizard'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import ModalOverlay from '@/Components/Modal/ModalOverlay'
import { c, jt } from 'ttag'
type Props = {
application: WebApplication
@@ -34,28 +35,27 @@ const Credentials: FunctionComponent<Props> = ({ application }: Props) => {
const closeChangeEmailDialog = () => setIsChangeEmailDialogOpen(false)
const emailSpan = <span className="wrap font-bold">{user?.email}</span>
const passwordDateSpan = <span className="font-bold">{passwordCreatedOn}</span>
return (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>Credentials</Title>
<Subtitle>Email</Subtitle>
<Text>
You're signed in as <span className="wrap font-bold">{user?.email}</span>
</Text>
<Title>{c('Title').t`Credentials`}</Title>
<Subtitle>{c('Subtitle').t`Email`}</Subtitle>
<Text>{jt`You're signed in as ${emailSpan}`}</Text>
<Button
className="mt-3 min-w-20"
label="Change email"
label={c('Action').t`Change email`}
onClick={() => {
setIsChangeEmailDialogOpen(true)
}}
/>
<HorizontalSeparator classes="my-4" />
<Subtitle>Password</Subtitle>
<Text>
Current password was set on <span className="font-bold">{passwordCreatedOn}</span>
</Text>
<Button className="mt-3 min-w-20" label="Change password" onClick={presentPasswordWizard} />
<Subtitle>{c('Subtitle').t`Password`}</Subtitle>
<Text>{jt`Current password was set on ${passwordDateSpan}`}</Text>
<Button className="mt-3 min-w-20" label={c('Action').t`Change password`} onClick={presentPasswordWizard} />
<ModalOverlay isOpen={isChangeEmailDialogOpen} close={closeChangeEmailDialog}>
<ChangeEmail onCloseDialog={closeChangeEmailDialog} application={application} />
</ModalOverlay>
@@ -4,6 +4,7 @@ import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Cont
import Button from '@/Components/Button/Button'
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
import { WebApplication } from '@/Application/WebApplication'
import { c } from 'ttag'
type Props = {
application: WebApplication
@@ -16,12 +17,13 @@ const DeleteAccount = ({ application }: Props) => {
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>Delete account</Title>
<Text>This action is irreversible. After deletion completes, you will be signed out on all devices.</Text>
<Title>{c('Title').t`Delete account`}</Title>
<Text>{c('Info')
.t`This action is irreversible. After deletion completes, you will be signed out on all devices.`}</Text>
<div className="mt-3 flex flex-row flex-wrap gap-3">
<Button
colorStyle="danger"
label="Delete my account"
label={c('Action').t`Delete my account`}
onClick={() => {
application.accountMenuController.setDeletingAccount(true)
}}
@@ -17,6 +17,7 @@ import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/Pre
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
import Spinner from '@/Components/Spinner/Spinner'
import NoProSubscription from '../NoProSubscription'
import { c, jt } from 'ttag'
type Props = {
application: WebApplication
@@ -37,7 +38,7 @@ const Email: FunctionComponent<Props> = ({ application }: Props) => {
await application.settings.updateSetting(settingName, payload, false)
return true
} catch (e) {
application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING).catch(console.error)
application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING()).catch(console.error)
return false
}
}
@@ -47,7 +48,7 @@ const Email: FunctionComponent<Props> = ({ application }: Props) => {
await application.settings.updateSubscriptionSetting(settingName, payload, false)
return true
} catch (e) {
application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING).catch(console.error)
application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING()).catch(console.error)
return false
}
}
@@ -117,27 +118,27 @@ const Email: FunctionComponent<Props> = ({ application }: Props) => {
}
}
const subscriptionBold = <span className="font-bold">{c('Label').t`subscription`}</span>
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>Email</Title>
<Title>{c('Title').t`Email`}</Title>
<div>
<div className="flex items-start justify-between gap-2 md:items-center">
<div className="flex flex-col">
<Subtitle>Mute sign-in notification emails</Subtitle>
<Subtitle>{c('Subtitle').t`Mute sign-in notification emails`}</Subtitle>
{isMuteSignInEmailsFeatureAvailable ? (
<Text>
Disables email notifications when a new sign-in occurs on your account. (Email notifications are
available only to paid subscribers).
{c('Info')
.t`Disables email notifications when a new sign-in occurs on your account. (Email notifications are available only to paid subscribers).`}
</Text>
) : (
<NoProSubscription
application={application}
text={
<span>
Sign-in notification emails are available only on a{' '}
<span className="font-bold">subscription</span> plan. Please upgrade in order to enable sign-in
notifications.
{jt`Sign-in notification emails are available only on a ${subscriptionBold} plan. Please upgrade in order to enable sign-in notifications.`}
</span>
}
/>
@@ -157,8 +158,8 @@ const Email: FunctionComponent<Props> = ({ application }: Props) => {
<HorizontalSeparator classes="my-4" />
<div className="flex items-start justify-between gap-2 md:items-center">
<div className="flex flex-col">
<Subtitle>Mute marketing notification emails</Subtitle>
<Text>Disables email notifications with special deals and promotions.</Text>
<Subtitle>{c('Subtitle').t`Mute marketing notification emails`}</Subtitle>
<Text>{c('Info').t`Disables email notifications with special deals and promotions.`}</Text>
</div>
{isLoading ? (
<Spinner className="h-5 w-5 flex-shrink-0" />
@@ -8,6 +8,7 @@ import { Subtitle, Title, Text } from '../../PreferencesComponents/Content'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import ClearSessionDataView from './ClearSessionDataView'
import { c } from 'ttag'
type Props = {
application: WebApplication
@@ -18,27 +19,27 @@ const SignOutView: FunctionComponent<Props> = observer(({ application }) => {
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>Sign out</Title>
<Subtitle>Other devices</Subtitle>
<Text>Want to sign out on all devices except this one?</Text>
<Title>{c('Title').t`Sign out`}</Title>
<Subtitle>{c('Subtitle').t`Other devices`}</Subtitle>
<Text>{c('Info').t`Want to sign out on all devices except this one?`}</Text>
<div className="mt-3 flex flex-row flex-wrap gap-3">
<Button
label="Sign out other sessions"
label={c('Action').t`Sign out other sessions`}
onClick={() => {
application.accountMenuController.setOtherSessionsSignOut(true)
}}
/>
<Button label="Manage sessions" onClick={() => application.openSessionsModal()} />
<Button label={c('Action').t`Manage sessions`} onClick={() => application.openSessionsModal()} />
</div>
</PreferencesSegment>
<HorizontalSeparator classes="my-4" />
<PreferencesSegment>
<Subtitle>This workspace</Subtitle>
<Text>Remove all data related to the current workspace from the application.</Text>
<Subtitle>{c('Subtitle').t`This workspace`}</Subtitle>
<Text>{c('Info').t`Remove all data related to the current workspace from the application.`}</Text>
<Button
className="mt-3"
colorStyle="danger"
label="Sign out workspace"
label={c('Action').t`Sign out workspace`}
onClick={() => {
application.accountMenuController.setSigningOut(true)
}}
@@ -9,6 +9,7 @@ import { WebApplication } from '@/Application/WebApplication'
import { formatLastSyncDate } from '@/Utils/DateUtils'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import { c, jt } from 'ttag'
type Props = {
application: WebApplication
@@ -27,24 +28,24 @@ const Sync: FunctionComponent<Props> = ({ application }: Props) => {
})
setIsSyncingInProgress(false)
if (response && (response as any).error) {
application.alerts.alert(STRING_GENERIC_SYNC_ERROR).catch(console.error)
application.alerts.alert(STRING_GENERIC_SYNC_ERROR()).catch(console.error)
} else {
setLastSyncDate(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
}
}
const lastSyncSpan = <span className="font-bold">{c('Info').t`on ${lastSyncDate}`}</span>
return (
<PreferencesGroup>
<PreferencesSegment>
<div className="flex flex-row items-center">
<div className="flex flex-grow flex-col">
<Title>Sync</Title>
<Text>
Last synced <span className="font-bold">on {lastSyncDate}</span>
</Text>
<Title>{c('Title').t`Sync`}</Title>
<Text>{jt`Last synced ${lastSyncSpan}`}</Text>
<Button
className="mt-3 min-w-20"
label="Sync now"
label={c('Action').t`Sync now`}
disabled={isSyncingInProgress}
onClick={doSynchronization}
/>
@@ -21,6 +21,7 @@ import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Spinner from '@/Components/Spinner/Spinner'
import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform'
import { c } from 'ttag'
type Props = {
application: WebApplication
@@ -104,7 +105,7 @@ const DataBackups = ({ application }: Props) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const readFile = async (file: File): Promise<any> => {
if (file.type === 'application/zip') {
application.alerts.alert(STRING_IMPORTING_ZIP_FILE).catch(console.error)
application.alerts.alert(STRING_IMPORTING_ZIP_FILE()).catch(console.error)
return
}
@@ -115,7 +116,7 @@ const DataBackups = ({ application }: Props) => {
const data = JSON.parse(e.target?.result as string)
resolve(data)
} catch (e) {
application.alerts.alert(STRING_INVALID_IMPORT_FILE).catch(console.error)
application.alerts.alert(STRING_INVALID_IMPORT_FILE()).catch(console.error)
}
}
reader.readAsText(file)
@@ -129,7 +130,7 @@ const DataBackups = ({ application }: Props) => {
setIsImportDataLoading(false)
let statusText = STRING_IMPORT_SUCCESS
let statusText = STRING_IMPORT_SUCCESS()
if (result.isFailed()) {
statusText = result.getError()
} else if (result.getValue().errorCount) {
@@ -162,7 +163,7 @@ const DataBackups = ({ application }: Props) => {
await performImport(data)
} else {
setIsImportDataLoading(false)
void alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION })
void alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION() })
}
}
@@ -187,32 +188,32 @@ const DataBackups = ({ application }: Props) => {
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>Data backups</Title>
<Subtitle>Download a backup of all your text-based data</Subtitle>
<Title>{c('Title').t`Data backups`}</Title>
<Subtitle>{c('Subtitle').t`Download a backup of all your text-based data`}</Subtitle>
{isEncryptionEnabled && (
<form className="sk-panel-form sk-panel-row">
<div className="flex items-center gap-2">
<label className="flex items-center gap-2">
<input type="radio" onChange={() => setIsBackupEncrypted(true)} checked={isBackupEncrypted} />
<span className="text-base font-medium md:text-sm">Encrypted</span>
<span className="text-base font-medium md:text-sm">{c('Label').t`Encrypted`}</span>
</label>
<label className="flex items-center gap-2">
<input type="radio" onChange={() => setIsBackupEncrypted(false)} checked={!isBackupEncrypted} />
<span className="text-base font-medium md:text-sm">Decrypted</span>
<span className="text-base font-medium md:text-sm">{c('Label').t`Decrypted`}</span>
</label>
</div>
</form>
)}
<Button onClick={downloadDataArchive} label="Download backup" className="mt-2" />
<Button onClick={downloadDataArchive} label={c('Action').t`Download backup`} className="mt-2" />
</PreferencesSegment>
<HorizontalSeparator classes="my-4" />
<PreferencesSegment>
<Subtitle>Import a previously saved backup file</Subtitle>
<Subtitle>{c('Subtitle').t`Import a previously saved backup file`}</Subtitle>
<div className="mt-3 flex flex-row items-center">
<Button label="Import backup" onClick={handleImportFile} />
<Button label={c('Action').t`Import backup`} onClick={handleImportFile} />
<input type="file" ref={fileInputRef} onChange={importFileSelected} className="hidden" />
{isImportDataLoading && <Spinner className="ml-4" />}
</div>
@@ -61,7 +61,7 @@ const EmailBackups = ({ application }: Props) => {
await application.settings.updateSetting(settingName, payload, false)
return true
} catch (e) {
application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING).catch(console.error)
application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING()).catch(console.error)
return false
}
}
@@ -24,7 +24,7 @@ const Privacy: FunctionComponent<Props> = ({ application }: Props) => {
await application.settings.updateSetting(settingName, payload, false)
return true
} catch (e) {
application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING).catch(console.error)
application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING()).catch(console.error)
return false
}
}
@@ -1,13 +1,15 @@
import { escapeHtmlString, Platform, SNApplication } from '@standardnotes/snjs'
import { getPlatform, isDesktopApplication } from '../Utils'
import { c } from 'ttag'
/** @generic */
export const STRING_SESSION_EXPIRED =
'Your session has expired. New changes will not be pulled in. Please sign in to refresh your session.'
export const STRING_DEFAULT_FILE_ERROR =
'Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.com/filesafe.'
export const STRING_GENERIC_SYNC_ERROR =
'There was an error syncing. Please try again. If all else fails, try signing out and signing back in.'
export const STRING_SESSION_EXPIRED = () =>
c('Error').t`Your session has expired. New changes will not be pulled in. Please sign in to refresh your session.`
export const STRING_DEFAULT_FILE_ERROR = () =>
c('Error')
.t`Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.com/filesafe.`
export const STRING_GENERIC_SYNC_ERROR = () =>
c('Error').t`There was an error syncing. Please try again. If all else fails, try signing out and signing back in.`
export function StringSyncException(data: unknown) {
return `There was an error while trying to save your items. Please contact support and share this message: ${JSON.stringify(
data,
@@ -51,8 +53,9 @@ export function StringEmptyTrash(count: number) {
/** @account */
export const STRING_ACCOUNT_MENU_UNCHECK_MERGE =
'Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?'
export const STRING_SIGN_OUT_CONFIRMATION =
'This action will remove this workspace and its related data from this device. Your synced data will not be affected.'
export const STRING_SIGN_OUT_CONFIRMATION = () =>
c('Info')
.t`This action will remove this workspace and its related data from this device. Your synced data will not be affected.`
export const STRING_ERROR_DECRYPTING_IMPORT =
'There was an error decrypting your items. Make sure the password you entered is correct and try again.'
export const STRING_E2E_ENABLED =
@@ -61,28 +64,34 @@ export const STRING_LOCAL_ENC_ENABLED =
'Encryption is enabled. Your data is encrypted using your passcode before it is saved to your device storage.'
export const STRING_ENC_NOT_ENABLED =
'Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption.'
export const STRING_IMPORT_SUCCESS = 'Your data has been successfully imported.'
export const STRING_IMPORT_SUCCESS = () => c('Info').t`Your data has been successfully imported.`
export const STRING_REMOVE_PASSCODE_CONFIRMATION = 'Are you sure you want to remove your application passcode?'
export const STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM = ' This will remove encryption from your local data.'
export const STRING_NON_MATCHING_PASSCODES = 'The two passcodes you entered do not match. Please try again.'
export const STRING_NON_MATCHING_PASSWORDS = 'The two passwords you entered do not match. Please try again.'
export const STRING_NON_MATCHING_PASSWORDS = () =>
c('Error').t`The two passwords you entered do not match. Please try again.`
export const STRING_GENERATING_LOGIN_KEYS = 'Generating Login Keys...'
export const STRING_GENERATING_REGISTER_KEYS = 'Generating Account Keys...'
export const STRING_INVALID_IMPORT_FILE = 'Unable to open file. Ensure it is a proper JSON file and try again.'
export const STRING_IMPORTING_ZIP_FILE =
'The file you selected is not a valid backup file. Please extract the contents of the zip file, then upload the contained .txt file.'
export const STRING_INVALID_IMPORT_FILE = () =>
c('Error').t`Unable to open file. Ensure it is a proper JSON file and try again.`
export const STRING_IMPORTING_ZIP_FILE = () =>
c('Error')
.t`The file you selected is not a valid backup file. Please extract the contents of the zip file, then upload the contained .txt file.`
export function StringImportError(errorCount: number) {
return `Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.`
return c('Info')
.t`Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.`
}
export const STRING_UNSUPPORTED_BACKUP_FILE_VERSION =
'This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again.'
export const STRING_UNSUPPORTED_BACKUP_FILE_VERSION = () =>
c('Error')
.t`This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again.`
/** @password_change */
export const STRING_FAILED_PASSWORD_CHANGE =
'There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.'
export const STRING_CONFIRM_APP_QUIT_DURING_UPGRADE =
'The encryption upgrade is in progress. You may lose data if you quit the app. ' + 'Are you sure you want to quit?'
export const STRING_CONFIRM_APP_QUIT_DURING_UPGRADE = () =>
c('Info')
.t`The encryption upgrade is in progress. You may lose data if you quit the app. Are you sure you want to quit?`
export const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE =
'A passcode change is in progress. You may lose data if you quit the app. ' + 'Are you sure you want to quit?'
@@ -90,21 +99,20 @@ export const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE =
export const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL =
'A passcode removal is in progress. You may lose data if you quit the app. ' + 'Are you sure you want to quit?'
export const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE = 'Encryption upgrade available'
export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT =
'Encryption version 004 is available. ' +
'This version strengthens the encryption algorithms your account and ' +
'local storage use. To learn more about this upgrade, visit our ' +
'<a href="https://standardnotes.com/help/security" target="_blank">Security Upgrade page.</a>'
export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = 'Upgrade'
export const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE = () => c('Title').t`Encryption upgrade available`
export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT = () =>
c('Info')
.t`Encryption version 004 is available. This version strengthens the encryption algorithms your account and local storage use. To learn more about this upgrade, visit our <a href="https://standardnotes.com/help/security" target="_blank">Security Upgrade page.</a>`
export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = () => c('Action').t`Upgrade`
export const STRING_REMOVE_OFFLINE_KEY_CONFIRMATION = 'This will delete the previously saved offline key.'
export const STRING_DELETE_ACCOUNT_CONFIRMATION =
"Are you sure you want to permanently delete your account? You will be asked to confirm your account password in the next step. If you have an active paid subscription, cancel the subscription first. Otherwise, if you'd like to keep the subscription, you can re-register with the same email after deletion, and your subscription will be linked back up with your account."
export const STRING_DELETE_ACCOUNT_CONFIRMATION = () =>
c('Info')
.t`Are you sure you want to permanently delete your account? You will be asked to confirm your account password in the next step. If you have an active paid subscription, cancel the subscription first. Otherwise, if you'd like to keep the subscription, you can re-register with the same email after deletion, and your subscription will be linked back up with your account.`
export const STRING_FAILED_TO_UPDATE_USER_SETTING =
'There was an error while trying to update your settings. Please try again.'
export const STRING_FAILED_TO_UPDATE_USER_SETTING = () =>
c('Error').t`There was an error while trying to update your settings. Please try again.`
export const Strings = {
protectingNoteWithoutProtectionSources: