Merge pull request #3936 from balena-io/i18n-conflict-resolve

I18n conflict resolve
This commit is contained in:
Balena CI 2022-12-14 11:15:48 -05:00 committed by GitHub
commit 898fe4f216
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 873 additions and 202 deletions

View File

@ -7,7 +7,8 @@ afterSign: ./afterSignHook.js
asar: false asar: false
files: files:
- generated - generated
- lib/shared/catalina-sudo/sudo-askpass.osascript.js - lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js
- lib/shared/catalina-sudo/sudo-askpass.osascript-en.js
mac: mac:
icon: assets/icon.icns icon: assets/icon.icns
category: public.app-category.developer-tools category: public.app-category.developer-tools

View File

@ -38,6 +38,7 @@ import * as osDialog from './os/dialog';
import * as windowProgress from './os/window-progress'; import * as windowProgress from './os/window-progress';
import MainPage from './pages/main/MainPage'; import MainPage from './pages/main/MainPage';
import './css/main.css'; import './css/main.css';
import * as i18next from 'i18next';
window.addEventListener( window.addEventListener(
'unhandledrejection', 'unhandledrejection',
@ -313,9 +314,9 @@ window.addEventListener('beforeunload', async (event) => {
try { try {
const confirmed = await osDialog.showWarning({ const confirmed = await osDialog.showWarning({
confirmationLabel: 'Yes, quit', confirmationLabel: i18next.t('yesExit'),
rejectionLabel: 'Cancel', rejectionLabel: i18next.t('cancel'),
title: 'Are you sure you want to close Etcher?', title: i18next.t('reallyExit'),
description: messages.warning.exitWhileFlashing(), description: messages.warning.exitWhileFlashing(),
}); });
if (confirmed) { if (confirmed) {

View File

@ -44,6 +44,7 @@ import {
import { SourceMetadata } from '../source-selector/source-selector'; import { SourceMetadata } from '../source-selector/source-selector';
import { middleEllipsis } from '../../utils/middle-ellipsis'; import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as i18next from 'i18next';
interface UsbbootDrive extends sourceDestination.UsbbootDrive { interface UsbbootDrive extends sourceDestination.UsbbootDrive {
progress: number; progress: number;
@ -189,7 +190,7 @@ export class DriveSelector extends React.Component<
this.tableColumns = [ this.tableColumns = [
{ {
field: 'description', field: 'description',
label: 'Name', label: i18next.t('drives.name'),
render: (description: string, drive: Drive) => { render: (description: string, drive: Drive) => {
if (isDrivelistDrive(drive)) { if (isDrivelistDrive(drive)) {
const isLargeDrive = isDriveSizeLarge(drive); const isLargeDrive = isDriveSizeLarge(drive);
@ -215,7 +216,7 @@ export class DriveSelector extends React.Component<
{ {
field: 'description', field: 'description',
key: 'size', key: 'size',
label: 'Size', label: i18next.t('drives.size'),
render: (_description: string, drive: Drive) => { render: (_description: string, drive: Drive) => {
if (isDrivelistDrive(drive) && drive.size !== null) { if (isDrivelistDrive(drive) && drive.size !== null) {
return prettyBytes(drive.size); return prettyBytes(drive.size);
@ -225,7 +226,7 @@ export class DriveSelector extends React.Component<
{ {
field: 'description', field: 'description',
key: 'link', key: 'link',
label: 'Location', label: i18next.t('drives.location'),
render: (_description: string, drive: Drive) => { render: (_description: string, drive: Drive) => {
return ( return (
<Txt> <Txt>
@ -399,14 +400,14 @@ export class DriveSelector extends React.Component<
color="#5b82a7" color="#5b82a7"
style={{ fontWeight: 600 }} style={{ fontWeight: 600 }}
> >
{drives.length} found {i18next.t('drives.find', { length: drives.length })}
</Txt> </Txt>
</Flex> </Flex>
} }
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>} titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
cancel={() => cancel(this.originalList)} cancel={() => cancel(this.originalList)}
done={() => done(selectedList)} done={() => done(selectedList)}
action={`Select (${selectedList.length})`} action={i18next.t('drives.select', { select: selectedList.length })}
primaryButtonProps={{ primaryButtonProps={{
primary: !showWarnings, primary: !showWarnings,
warning: showWarnings, warning: showWarnings,
@ -512,7 +513,11 @@ export class DriveSelector extends React.Component<
> >
<Flex alignItems="center"> <Flex alignItems="center">
<ChevronDownSvg height="1em" fill="currentColor" /> <ChevronDownSvg height="1em" fill="currentColor" />
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt> <Txt ml={8}>
{i18next.t('drives.showHidden', {
num: numberOfHiddenSystemDrives,
})}
</Txt>
</Flex> </Flex>
</Link> </Link>
)} )}
@ -520,7 +525,7 @@ export class DriveSelector extends React.Component<
)} )}
{this.props.showWarnings && hasSystemDrives ? ( {this.props.showWarnings && hasSystemDrives ? (
<Alert className="system-drive-alert" style={{ width: '67%' }}> <Alert className="system-drive-alert" style={{ width: '67%' }}>
Selecting your system drive is dangerous and will erase your drive! {i18next.t('drives.systemDriveDanger')}
</Alert> </Alert>
) : null} ) : null}
@ -540,13 +545,15 @@ export class DriveSelector extends React.Component<
this.setState({ missingDriversModal: {} }); this.setState({ missingDriversModal: {} });
} }
}} }}
action="Yes, continue" action={i18next.t('yesContinue')}
cancelButtonProps={{ cancelButtonProps={{
children: 'Cancel', children: i18next.t('cancel'),
}} }}
children={ children={
missingDriversModal.drive.linkMessage || missingDriversModal.drive.linkMessage ||
`Etcher will open ${missingDriversModal.drive.link} in your browser` i18next.t('drives.openInBrowser', {
link: missingDriversModal.drive.link,
})
} }
/> />
)} )}

View File

@ -7,6 +7,7 @@ import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as prettyBytes from 'pretty-bytes'; import * as prettyBytes from 'pretty-bytes';
import { DriveWithWarnings } from '../../pages/main/Flash'; import { DriveWithWarnings } from '../../pages/main/Flash';
import * as i18next from 'i18next';
const DriveStatusWarningModal = ({ const DriveStatusWarningModal = ({
done, done,
@ -17,12 +18,12 @@ const DriveStatusWarningModal = ({
isSystem: boolean; isSystem: boolean;
drivesWithWarnings: DriveWithWarnings[]; drivesWithWarnings: DriveWithWarnings[];
}) => { }) => {
let warningSubtitle = 'You are about to erase an unusually large drive'; let warningSubtitle = i18next.t('drives.largeDriveWarning');
let warningCta = 'Are you sure the selected drive is not a storage drive?'; let warningCta = i18next.t('drives.largeDriveWarningMsg');
if (isSystem) { if (isSystem) {
warningSubtitle = "You are about to erase your computer's drives"; warningSubtitle = i18next.t('drives.systemDriveWarning');
warningCta = 'Are you sure you want to flash your system drive?'; warningCta = i18next.t('drives.systemDriveWarningMsg');
} }
return ( return (
<Modal <Modal
@ -33,9 +34,9 @@ const DriveStatusWarningModal = ({
cancelButtonProps={{ cancelButtonProps={{
primary: false, primary: false,
warning: true, warning: true,
children: 'Change target', children: i18next.t('drives.changeTarget'),
}} }}
action={"Yes, I'm sure"} action={i18next.t('sure')}
primaryButtonProps={{ primaryButtonProps={{
primary: false, primary: false,
outline: true, outline: true,
@ -50,7 +51,7 @@ const DriveStatusWarningModal = ({
<Flex flexDirection="column"> <Flex flexDirection="column">
<ExclamationTriangleSvg height="2em" fill="#fca321" /> <ExclamationTriangleSvg height="2em" fill="#fca321" />
<Txt fontSize="24px" color="#fca321"> <Txt fontSize="24px" color="#fca321">
WARNING! {i18next.t('warning')}
</Txt> </Txt>
</Flex> </Flex>
<Txt fontSize="24px">{warningSubtitle}</Txt> <Txt fontSize="24px">{warningSubtitle}</Txt>

View File

@ -17,6 +17,7 @@
import * as React from 'react'; import * as React from 'react';
import { BaseButton } from '../../styled-components'; import { BaseButton } from '../../styled-components';
import * as i18next from 'i18next';
export interface FlashAnotherProps { export interface FlashAnotherProps {
onClick: () => void; onClick: () => void;
@ -25,7 +26,7 @@ export interface FlashAnotherProps {
export const FlashAnother = (props: FlashAnotherProps) => { export const FlashAnother = (props: FlashAnotherProps) => {
return ( return (
<BaseButton primary onClick={props.onClick}> <BaseButton primary onClick={props.onClick}>
Flash another {i18next.t('flash.another')}
</BaseButton> </BaseButton>
); );
}; };

View File

@ -31,6 +31,7 @@ import { resetState } from '../../models/flash-state';
import * as selection from '../../models/selection-state'; import * as selection from '../../models/selection-state';
import { middleEllipsis } from '../../utils/middle-ellipsis'; import { middleEllipsis } from '../../utils/middle-ellipsis';
import { Modal, Table } from '../../styled-components'; import { Modal, Table } from '../../styled-components';
import * as i18next from 'i18next';
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)` const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
&&& [data-display='table-head'], &&& [data-display='table-head'],
@ -88,15 +89,15 @@ function formattedErrors(errors: FlashError[]) {
const columns: Array<TableColumn<FlashError>> = [ const columns: Array<TableColumn<FlashError>> = [
{ {
field: 'description', field: 'description',
label: 'Target', label: i18next.t('flash.target'),
}, },
{ {
field: 'device', field: 'device',
label: 'Location', label: i18next.t('flash.location'),
}, },
{ {
field: 'message', field: 'message',
label: 'Error', label: i18next.t('flash.error'),
render: (message: string, { code }: FlashError) => { render: (message: string, { code }: FlashError) => {
return message ?? code; return message ?? code;
}, },
@ -162,9 +163,11 @@ export function FlashResults({
<Txt>{middleEllipsis(image, 24)}</Txt> <Txt>{middleEllipsis(image, 24)}</Txt>
</Flex> </Flex>
<Txt fontSize={24} color="#fff" mb="17px"> <Txt fontSize={24} color="#fff" mb="17px">
Flash {allFailed ? 'Failed' : 'Complete'}! {allFailed
? i18next.t('flash.flashFailed')
: i18next.t('flash.flashCompleted')}
</Txt> </Txt>
{skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null} {skip ? <Txt color="#7e8085">{i18next.t('flash.skip')}</Txt> : null}
</Flex> </Flex>
<Flex flexDirection="column" color="#7e8085"> <Flex flexDirection="column" color="#7e8085">
{results.devices.successful !== 0 ? ( {results.devices.successful !== 0 ? (
@ -188,7 +191,7 @@ export function FlashResults({
{progress.failed(errors.length)} {progress.failed(errors.length)}
</Txt> </Txt>
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}> <Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
more info {i18next.t('flash.moreInfo')}
</Link> </Link>
</Flex> </Flex>
) : null} ) : null}
@ -199,12 +202,9 @@ export function FlashResults({
fontWeight: 500, fontWeight: 500,
textAlign: 'center', textAlign: 'center',
}} }}
tooltip={outdent({ newline: ' ' })` tooltip={i18next.t('flash.speedTip')}
The speed is calculated by dividing the image size by the flashing time.
Disk images with ext partitions flash faster as we are able to skip unused parts.
`}
> >
Effective speed: {effectiveSpeed} MB/s {i18next.t('flash.speed', { speed: effectiveSpeed })}
</Txt> </Txt>
)} )}
</Flex> </Flex>
@ -214,11 +214,11 @@ export function FlashResults({
titleElement={ titleElement={
<Flex alignItems="baseline" mb={18}> <Flex alignItems="baseline" mb={18}>
<Txt fontSize={24} align="left"> <Txt fontSize={24} align="left">
Failed targets {i18next.t('failedTarget')}
</Txt> </Txt>
</Flex> </Flex>
} }
action="Retry failed targets" action={i18next.t('failedRetry')}
cancel={() => setShowErrorsInfo(false)} cancel={() => setShowErrorsInfo(false)}
done={() => { done={() => {
setShowErrorsInfo(false); setShowErrorsInfo(false);

View File

@ -20,6 +20,7 @@ import { default as styled } from 'styled-components';
import { fromFlashState } from '../../modules/progress-status'; import { fromFlashState } from '../../modules/progress-status';
import { StepButton } from '../../styled-components'; import { StepButton } from '../../styled-components';
import * as i18next from 'i18next';
const FlashProgressBar = styled(ProgressBar)` const FlashProgressBar = styled(ProgressBar)`
> div { > div {
@ -28,6 +29,7 @@ const FlashProgressBar = styled(ProgressBar)`
color: white !important; color: white !important;
text-shadow: none !important; text-shadow: none !important;
transition-duration: 0s; transition-duration: 0s;
> div { > div {
transition-duration: 0s; transition-duration: 0s;
} }
@ -61,7 +63,7 @@ const colors = {
} as const; } as const;
const CancelButton = styled(({ type, onClick, ...props }) => { const CancelButton = styled(({ type, onClick, ...props }) => {
const status = type === 'verifying' ? 'Skip' : 'Cancel'; const status = type === 'verifying' ? i18next.t('skip') : i18next.t('cancel');
return ( return (
<Button plain onClick={() => onClick(status)} {...props}> <Button plain onClick={() => onClick(status)} {...props}>
{status} {status}
@ -69,6 +71,7 @@ const CancelButton = styled(({ type, onClick, ...props }) => {
); );
})` })`
font-weight: 600; font-weight: 600;
&&& { &&& {
width: auto; width: auto;
height: auto; height: auto;
@ -126,7 +129,7 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
marginTop: 30, marginTop: 30,
}} }}
> >
Flash! {i18next.t('flash.flashNow')}
</StepButton> </StepButton>
); );
} }

View File

@ -24,6 +24,7 @@ import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external'; import { open as openExternal } from '../../os/open-external/services/open-external';
import { Modal } from '../../styled-components'; import { Modal } from '../../styled-components';
import * as i18next from 'i18next';
interface Setting { interface Setting {
name: string; name: string;
@ -34,13 +35,13 @@ async function getSettingsList(): Promise<Setting[]> {
const list: Setting[] = [ const list: Setting[] = [
{ {
name: 'errorReporting', name: 'errorReporting',
label: 'Anonymously report errors and usage statistics to balena.io', label: i18next.t('settings.errorReporting'),
}, },
]; ];
if (['appimage', 'nsis', 'dmg'].includes(packageType)) { if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
list.push({ list.push({
name: 'updatesEnabled', name: 'updatesEnabled',
label: 'Auto-updates enabled', label: i18next.t('settings.autoUpdate'),
}); });
} }
return list; return list;
@ -58,6 +59,7 @@ const InfoBox = (props: any) => (
<TextWithCopy code text={props.value} copy={props.value} /> <TextWithCopy code text={props.value} copy={props.value} />
</Box> </Box>
); );
export function SettingsModal({ toggleModal }: SettingsModalProps) { export function SettingsModal({ toggleModal }: SettingsModalProps) {
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]); const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
React.useEffect(() => { React.useEffect(() => {
@ -92,7 +94,7 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
<Modal <Modal
titleElement={ titleElement={
<Txt fontSize={24} mb={24}> <Txt fontSize={24} mb={24}>
Settings {i18next.t('settings.settings')}
</Txt> </Txt>
} }
done={() => toggleModal(false)} done={() => toggleModal(false)}
@ -113,7 +115,7 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
})} })}
{UUID !== undefined && ( {UUID !== undefined && (
<Flex flexDirection="column"> <Flex flexDirection="column">
<Txt fontSize={24}>System Information</Txt> <Txt fontSize={24}>{i18next.t('settings.systemInformation')}</Txt>
<InfoBox label="UUID" value={UUID.substr(0, 7)} /> <InfoBox label="UUID" value={UUID.substr(0, 7)} />
</Flex> </Flex>
)} )}

View File

@ -66,6 +66,7 @@ import { DriveSelector } from '../drive-selector/drive-selector';
import { DrivelistDrive } from '../../../../shared/drive-constraints'; import { DrivelistDrive } from '../../../../shared/drive-constraints';
import axios, { AxiosRequestConfig } from 'axios'; import axios, { AxiosRequestConfig } from 'axios';
import { isJson } from '../../../../shared/utils'; import { isJson } from '../../../../shared/utils';
import * as i18next from 'i18next';
const recentUrlImagesKey = 'recentUrlImages'; const recentUrlImagesKey = 'recentUrlImages';
@ -160,7 +161,7 @@ const URLSelector = ({
primaryButtonProps={{ primaryButtonProps={{
disabled: loading || !imageURL, disabled: loading || !imageURL,
}} }}
action={loading ? <Spinner /> : 'OK'} action={loading ? <Spinner /> : i18next.t('ok')}
done={async () => { done={async () => {
setLoading(true); setLoading(true);
const urlStrings = recentImages.map((url: URL) => url.href); const urlStrings = recentImages.map((url: URL) => url.href);
@ -176,11 +177,11 @@ const URLSelector = ({
<Flex flexDirection="column"> <Flex flexDirection="column">
<Flex mb={15} style={{ width: '100%' }} flexDirection="column"> <Flex mb={15} style={{ width: '100%' }} flexDirection="column">
<Txt mb="10px" fontSize="24px"> <Txt mb="10px" fontSize="24px">
Use Image URL {i18next.t('source.useSourceURL')}
</Txt> </Txt>
<Input <Input
value={imageURL} value={imageURL}
placeholder="Enter a valid URL" placeholder={i18next.t('source.enterValidURL')}
type="text" type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) => onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setImageURL(evt.target.value) setImageURL(evt.target.value)
@ -205,7 +206,7 @@ const URLSelector = ({
{!showBasicAuth && ( {!showBasicAuth && (
<ChevronRightSvg height="1em" fill="currentColor" /> <ChevronRightSvg height="1em" fill="currentColor" />
)} )}
<Txt ml={8}>Authentication</Txt> <Txt ml={8}>{i18next.t('source.auth')}</Txt>
</Flex> </Flex>
</Link> </Link>
{showBasicAuth && ( {showBasicAuth && (
@ -213,7 +214,7 @@ const URLSelector = ({
<Input <Input
mb={15} mb={15}
value={username} value={username}
placeholder="Enter username" placeholder={i18next.t('source.username')}
type="text" type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) => onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setUsername(evt.target.value) setUsername(evt.target.value)
@ -221,7 +222,7 @@ const URLSelector = ({
/> />
<Input <Input
value={password} value={password}
placeholder="Enter password" placeholder={i18next.t('source.password')}
type="password" type="password"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) => onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setPassword(evt.target.value) setPassword(evt.target.value)
@ -295,7 +296,7 @@ const FlowSelector = styled(
font-weight: 600; font-weight: 600;
svg { svg {
color: ${colors.primary.foreground}!important; color: ${colors.primary.foreground} !important;
} }
} }
`; `;
@ -453,7 +454,7 @@ export class SourceSelector extends React.Component<
!isURL(this.normalizeImagePath(selected)) !isURL(this.normalizeImagePath(selected))
) { ) {
this.handleError( this.handleError(
'Unsupported protocol', i18next.t('source.unsupportedProtocol'),
selected, selected,
messages.error.unsupportedProtocol(), messages.error.unsupportedProtocol(),
); );
@ -465,7 +466,7 @@ export class SourceSelector extends React.Component<
this.setState({ this.setState({
warning: { warning: {
message: messages.warning.looksLikeWindowsImage(), message: messages.warning.looksLikeWindowsImage(),
title: 'Possible Windows image detected', title: i18next.t('source.windowsImage'),
}, },
}); });
} }
@ -491,13 +492,13 @@ export class SourceSelector extends React.Component<
this.setState({ this.setState({
warning: { warning: {
message: messages.warning.missingPartitionTable(), message: messages.warning.missingPartitionTable(),
title: 'Missing partition table', title: i18next.t('source.partitionTable'),
}, },
}); });
} }
} catch (error: any) { } catch (error: any) {
this.handleError( this.handleError(
'Error opening source', i18next.t('source.errorOpen'),
sourcePath, sourcePath,
messages.error.openSource(sourcePath, error.message), messages.error.openSource(sourcePath, error.message),
error, error,
@ -515,7 +516,7 @@ export class SourceSelector extends React.Component<
this.setState({ this.setState({
warning: { warning: {
message: messages.warning.driveMissingPartitionTable(), message: messages.warning.driveMissingPartitionTable(),
title: 'Missing partition table', title: i18next.t('source.partitionTable'),
}, },
}); });
} }
@ -719,7 +720,7 @@ export class SourceSelector extends React.Component<
mb={14} mb={14}
onClick={() => this.reselectSource()} onClick={() => this.reselectSource()}
> >
Remove {i18next.t('cancel')}
</ChangeButton> </ChangeButton>
)} )}
{!_.isNil(imageSize) && !imageLoading && ( {!_.isNil(imageSize) && !imageLoading && (
@ -734,7 +735,7 @@ export class SourceSelector extends React.Component<
key="Flash from file" key="Flash from file"
flow={{ flow={{
onClick: () => this.openImageSelector(), onClick: () => this.openImageSelector(),
label: 'Flash from file', label: i18next.t('source.fromFile'),
icon: <FileSvg height="1em" fill="currentColor" />, icon: <FileSvg height="1em" fill="currentColor" />,
}} }}
onMouseEnter={() => this.setDefaultFlowActive(false)} onMouseEnter={() => this.setDefaultFlowActive(false)}
@ -744,7 +745,7 @@ export class SourceSelector extends React.Component<
key="Flash from URL" key="Flash from URL"
flow={{ flow={{
onClick: () => this.openURLSelector(), onClick: () => this.openURLSelector(),
label: 'Flash from URL', label: i18next.t('source.fromURL'),
icon: <LinkSvg height="1em" fill="currentColor" />, icon: <LinkSvg height="1em" fill="currentColor" />,
}} }}
onMouseEnter={() => this.setDefaultFlowActive(false)} onMouseEnter={() => this.setDefaultFlowActive(false)}
@ -754,7 +755,7 @@ export class SourceSelector extends React.Component<
key="Clone drive" key="Clone drive"
flow={{ flow={{
onClick: () => this.openDriveSelector(), onClick: () => this.openDriveSelector(),
label: 'Clone drive', label: i18next.t('source.clone'),
icon: <CopySvg height="1em" fill="currentColor" />, icon: <CopySvg height="1em" fill="currentColor" />,
}} }}
onMouseEnter={() => this.setDefaultFlowActive(false)} onMouseEnter={() => this.setDefaultFlowActive(false)}
@ -775,7 +776,7 @@ export class SourceSelector extends React.Component<
<span>{this.state.warning.title}</span> <span>{this.state.warning.title}</span>
</span> </span>
} }
action="Continue" action={i18next.t('continue')}
cancel={() => { cancel={() => {
this.setState({ warning: null }); this.setState({ warning: null });
this.reselectSource(); this.reselectSource();
@ -793,17 +794,17 @@ export class SourceSelector extends React.Component<
{showImageDetails && ( {showImageDetails && (
<SmallModal <SmallModal
title="Image" title={i18next.t('source.image')}
done={() => { done={() => {
this.setState({ showImageDetails: false }); this.setState({ showImageDetails: false });
}} }}
> >
<Txt.p> <Txt.p>
<Txt.span bold>Name: </Txt.span> <Txt.span bold>{i18next.t('source.name')}</Txt.span>
<Txt.span>{imageName || imageBasename}</Txt.span> <Txt.span>{imageName || imageBasename}</Txt.span>
</Txt.p> </Txt.p>
<Txt.p> <Txt.p>
<Txt.span bold>Path: </Txt.span> <Txt.span bold>{i18next.t('source.path')}</Txt.span>
<Txt.span>{imagePath}</Txt.span> <Txt.span>{imagePath}</Txt.span>
</Txt.p> </Txt.p>
</SmallModal> </SmallModal>
@ -842,8 +843,8 @@ export class SourceSelector extends React.Component<
<DriveSelector <DriveSelector
write={false} write={false}
multipleSelection={false} multipleSelection={false}
titleLabel="Select source" titleLabel={i18next.t('source.selectSource')}
emptyListLabel="Plug a source drive" emptyListLabel={i18next.t('source.plugSource')}
emptyListIcon={<SrcSvg width="40px" />} emptyListIcon={<SrcSvg width="40px" />}
cancel={(originalList) => { cancel={(originalList) => {
if (originalList.length) { if (originalList.length) {

View File

@ -32,6 +32,7 @@ import {
StepNameButton, StepNameButton,
} from '../../styled-components'; } from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis'; import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as i18next from 'i18next';
interface TargetSelectorProps { interface TargetSelectorProps {
targets: any[]; targets: any[];
@ -95,7 +96,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
</StepNameButton> </StepNameButton>
{!props.flashing && ( {!props.flashing && (
<ChangeButton plain mb={14} onClick={props.reselectDrive}> <ChangeButton plain mb={14} onClick={props.reselectDrive}>
Change {i18next.t('target.change')}
</ChangeButton> </ChangeButton>
)} )}
{target.size != null && ( {target.size != null && (
@ -132,11 +133,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
return ( return (
<> <>
<StepNameButton plain tooltip={props.tooltip}> <StepNameButton plain tooltip={props.tooltip}>
{targets.length} Targets {targets.length} {i18next.t('target.targets')}
</StepNameButton> </StepNameButton>
{!props.flashing && ( {!props.flashing && (
<ChangeButton plain onClick={props.reselectDrive} mb={14}> <ChangeButton plain onClick={props.reselectDrive} mb={14}>
Change {i18next.t('target.change')}
</ChangeButton> </ChangeButton>
)} )}
{targetsTemplate} {targetsTemplate}
@ -151,7 +152,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
disabled={props.disabled} disabled={props.disabled}
onClick={props.openDriveSelector} onClick={props.openDriveSelector}
> >
Select target {i18next.t('target.selectTarget')}
</StepButton> </StepButton>
); );
} }

View File

@ -37,6 +37,7 @@ import TgtSvg from '../../../assets/tgt.svg';
import DriveSvg from '../../../assets/drive.svg'; import DriveSvg from '../../../assets/drive.svg';
import { warning } from '../../../../shared/messages'; import { warning } from '../../../../shared/messages';
import { DrivelistDrive } from '../../../../shared/drive-constraints'; import { DrivelistDrive } from '../../../../shared/drive-constraints';
import * as i18next from 'i18next';
export const getDriveListLabel = () => { export const getDriveListLabel = () => {
return getSelectedDrives() return getSelectedDrives()
@ -60,8 +61,8 @@ export const TargetSelectorModal = (
) => ( ) => (
<DriveSelector <DriveSelector
multipleSelection={true} multipleSelection={true}
titleLabel="Select target" titleLabel={i18next.t('target.selectTarget')}
emptyListLabel="Plug a target drive" emptyListLabel={i18next.t('target.plugTarget')}
emptyListIcon={<TgtSvg width="40px" />} emptyListIcon={<TgtSvg width="40px" />}
showWarnings={true} showWarnings={true}
selectedList={getSelectedDrives()} selectedList={getSelectedDrives()}

42
lib/gui/app/i18n.ts Normal file
View File

@ -0,0 +1,42 @@
import * as i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import zh_CN_translation from './i18n/zh-CN';
import zh_TW_translation from './i18n/zh-TW';
import en_translation from './i18n/en';
export function langParser() {
if (process.env.LANG !== undefined) {
// Bypass mocha, where lang-detect don't works
return 'en';
}
const lang = Intl.DateTimeFormat().resolvedOptions().locale;
switch (lang.substr(0, 2)) {
case 'zh':
if (lang === 'zh-CN' || lang === 'zh-SG') {
return 'zh-CN';
} // Simplified Chinese
else {
return 'zh-TW';
} // Traditional Chinese
default:
return lang.substr(0, 2);
}
}
i18next.use(initReactI18next).init({
lng: langParser(),
fallbackLng: 'en',
nonExplicitSupportedLngs: true,
interpolation: {
escapeValue: false,
},
resources: {
'zh-CN': zh_CN_translation,
'zh-TW': zh_TW_translation,
en: en_translation,
},
});
export default i18next;

View File

@ -0,0 +1,23 @@
# i18n
## How it was done
Using the open-source lib [i18next](https://www.i18next.com/).
## How to add your own language
1. Go to `lib/gui/app/i18n` and add a file named `xx.ts` (use the codes mentioned
in [the link](https://www.science.co.il/language/Locale-codes.php), and we support styles as `fr`, `de`, `es-ES`
and `pt-BR`)
.
2. Copy the content from an existing translation and start to translate.
3. Once done, go to `lib/gui/app/i18n.ts` and add a line of `import xx_translation from './i18n/xx'` after the
already-added imports and add `xx: xx_translation` in the `resources` section of `i18next.init()` function.
4. Now go to `lib/shared/catalina-sudo/` and copy the `sudo-askpass.osascript-en.js`, change it to
be `sudo-askpass.osascript-xx.js` and edit
the `'balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.'` line and
those `Ok`s and `Cancel`s to your own language.
5. If, your language has several variations when they are used in several countries/regions, such as `zh-CN` and `zh-TW`
, or `pt-BR` and `pt-PT`, edit
the `langParser()` in the `lib/gui/app/i18n.ts` file to meet your need.
6. Make a commit, and then a pull request on GitHub.

160
lib/gui/app/i18n/en.ts Normal file
View File

@ -0,0 +1,160 @@
const translation = {
translation: {
continue: 'Continue',
ok: 'OK',
cancel: 'Cancel',
skip: 'Skip',
sure: "Yes, I'm sure",
warning: 'WARNING! ',
attention: 'Attention',
failed: 'Failed',
completed: 'Completed',
yesContinue: 'Yes, continue',
reallyExit: 'Are you sure you want to close Etcher?',
yesExit: 'Yes, quit',
progress: {
starting: 'Starting...',
decompressing: 'Decompressing...',
flashing: 'Flashing...',
finishing: 'Finishing...',
verifying: 'Validating...',
failing: 'Failed',
},
message: {
sizeNotRecommended: 'Not recommended',
tooSmall: 'Too small',
locked: 'Locked',
system: 'System drive',
containsImage: 'Source drive',
largeDrive: 'Large drive',
sourceLarger: 'The selected source is {{byte}} larger than this drive.',
flashSucceed_one: 'Successful target',
flashSucceed_other: 'Successful targets',
flashFail_one: 'Failed target',
flashFail_other: 'Failed targets',
toDrive: 'to {{description}} ({{name}})',
toTarget_one: 'to {{num}} target',
toTarget_other: 'to {{num}} targets',
andFailTarget_one: 'and failed to be flashed to {{num}} target',
andFailTarget_other: 'and failed to be flashed to {{num}} targets',
succeedTo: '{{name}} was successfully flashed {{target}}',
exitWhileFlashing:
'You are currently flashing a drive. Closing Etcher may leave your drive in an unusable state.',
looksLikeWindowsImage:
'It looks like you are trying to burn a Windows image.\n\nUnlike other images, Windows images require special processing to be made bootable. We suggest you use a tool specially designed for this purpose, such as <a href="https://rufus.akeo.ie">Rufus</a> (Windows), <a href="https://github.com/slacka/WoeUSB">WoeUSB</a> (Linux), or Boot Camp Assistant (macOS).',
image: 'image',
drive: 'drive',
missingPartitionTable:
'It looks like this is not a bootable {{type}}.\n\nThe {{type}} does not appear to contain a partition table, and might not be recognized or bootable by your device.',
largeDriveSize:
"This is a large drive! Make sure it doesn't contain files that you want to keep.",
systemDrive:
'Selecting your system drive is dangerous and will erase your drive!',
sourceDrive: 'Contains the image you chose to flash',
noSpace:
'Not enough space on the drive. Please insert larger one and try again.',
genericFlashError:
'Something went wrong. If it is a compressed image, please check that the archive is not corrupted.\n{{error}}',
validation:
'The write has been completed successfully but Etcher detected potential corruption issues when reading the image back from the drive. \n\nPlease consider writing the image to a different drive.',
openError:
'Something went wrong while opening {{source}}.\n\nError: {{error}}',
flashError: 'Something went wrong while writing {{image}} {{targets}}.',
unplug:
"Looks like Etcher lost access to the drive. Did it get unplugged accidentally?\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.",
cannotWrite:
'Looks like Etcher is not able to write to this location of the drive. This error is usually caused by a faulty drive, reader, or port. \n\nPlease try again with another drive, reader, or port.',
childWriterDied:
'The writer process ended unexpectedly. Please try again, and contact the Etcher team if the problem persists.',
badProtocol: 'Only http:// and https:// URLs are supported.',
},
target: {
selectTarget: 'Select target',
plugTarget: 'Plug a target drive',
targets: 'Targets',
change: 'Change',
},
source: {
useSourceURL: 'Use Image URL',
auth: 'Authentication',
username: 'Enter username',
password: 'Enter password',
unsupportedProtocol: 'Unsupported protocol',
windowsImage: 'Possible Windows image detected',
partitionTable: 'Missing partition table',
errorOpen: 'Error opening source',
fromFile: 'Flash from file',
fromURL: 'Flash from URL',
clone: 'Clone drive',
image: 'Image',
name: 'Name: ',
path: 'Path: ',
selectSource: 'Select source',
plugSource: 'Plug a source drive',
osImages: 'OS Images',
allFiles: 'All',
enterValidURL: 'Enter a valid URL',
},
drives: {
name: 'Name',
size: 'Size',
location: 'Location',
find: '{{length}} found',
select: 'Select {{select}}',
showHidden: 'Show {{num}} hidden',
systemDriveDanger:
'Selecting your system drive is dangerous and will erase your drive!',
openInBrowser: '`Etcher will open {{link}} in your browser`',
changeTarget: 'Change target',
largeDriveWarning: 'You are about to erase an unusually large drive',
largeDriveWarningMsg:
'Are you sure the selected drive is not a storage drive?',
systemDriveWarning: "You are about to erase your computer's drives",
systemDriveWarningMsg:
'Are you sure you want to flash your system drive?',
},
flash: {
another: 'Flash another',
target: 'Target',
location: 'Location',
error: 'Error',
flash: 'Flash',
flashNow: 'Flash!',
skip: 'Validation has been skipped',
moreInfo: 'more info',
speedTip:
'The speed is calculated by dividing the image size by the flashing time.\nDisk images with ext partitions flash faster as we are able to skip unused parts.',
speed: 'Effective speed: {{speed}} MB/s',
speedShort: '{{speed}} MB/s',
eta: 'ETA: {{eta}}',
failedTarget: 'Failed targets',
failedRetry: 'Retry failed targets',
flashFailed: 'Flash Failed.',
flashCompleted: 'Flash Completed!',
},
settings: {
errorReporting:
'Anonymously report errors and usage statistics to balena.io',
autoUpdate: 'Auto-updates enabled',
settings: 'Settings',
systemInformation: 'System Information',
},
menu: {
edit: 'Edit',
view: 'View',
devTool: 'Toggle Developer Tools',
window: 'Window',
help: 'Help',
pro: 'Etcher Pro',
website: 'Etcher Website',
issue: 'Report an issue',
about: 'About Etcher',
hide: 'Hide Etcher',
hideOthers: 'Hide Others',
unhide: 'Unhide All',
quit: 'Quit Etcher',
},
},
};
export default translation;

152
lib/gui/app/i18n/zh-CN.ts Normal file
View File

@ -0,0 +1,152 @@
const translation = {
translation: {
ok: '好',
cancel: '取消',
continue: '继续',
skip: '跳过',
sure: '我确定',
warning: '请注意!',
attention: '请注意',
failed: '失败',
completed: '完毕',
yesExit: '是的,可以退出',
reallyExit: '真的要现在退出 Etcher 吗?',
yesContinue: '是的,继续',
progress: {
starting: '正在启动……',
decompressing: '正在解压……',
flashing: '正在烧录……',
finishing: '正在结束……',
verifying: '正在验证……',
failing: '失败……',
},
message: {
sizeNotRecommended: '大小不推荐',
tooSmall: '空间太小',
locked: '被锁定',
system: '系统盘',
containsImage: '存放源镜像',
largeDrive: '很大的磁盘',
sourceLarger: '所选的镜像比目标盘大了 {{byte}} 比特。',
flashSucceed_one: '烧录成功',
flashSucceed_other: '烧录成功',
flashFail_one: '烧录失败',
flashFail_other: '烧录失败',
toDrive: '到 {{description}} ({{name}})',
toTarget_one: '到 {{num}} 个目标',
toTarget_other: '到 {{num}} 个目标',
andFailTarget_one: '并烧录失败了 {{num}} 个目标',
andFailTarget_other: '并烧录失败了 {{num}} 个目标',
succeedTo: '{{name}} 被成功烧录 {{target}}',
exitWhileFlashing:
'您当前正在刷机。 关闭 Etcher 可能会导致您的磁盘无法使用。',
looksLikeWindowsImage:
'看起来您正在尝试刻录 Windows 镜像。\n\n与其他镜像不同Windows 镜像需要特殊处理才能使其可启动。 我们建议您使用专门为此目的设计的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
image: '镜像',
drive: '磁盘',
missingPartitionTable:
'看起来这不是一个可启动的{{type}}。\n\n这个{{type}}似乎不包含分区表,因此您的设备可能无法识别或无法正确启动。',
largeDriveSize: '这是个很大的磁盘!请检查并确认它不包含对您很重要的信息',
systemDrive: '选择系统盘很危险,因为这将会删除你的系统',
sourceDrive: '源镜像位于这个分区中',
noSpace: '磁盘空间不足。 请插入另一个较大的磁盘并重试。',
genericFlashError:
'出了点问题。如果源镜像曾被压缩过,请检查它是否已损坏。\n{{error}}',
validation:
'写入已成功完成,但 Etcher 在从磁盘读取镜像时检测到潜在的损坏问题。 \n\n请考虑将镜像写入其他磁盘。',
openError: '打开 {{source}} 时出错。\n\n错误信息 {{error}}',
flashError: '烧录 {{image}} {{targets}} 失败。',
unplug:
'看起来 Etcher 失去了对磁盘的连接。 它是不是被意外拔掉了?\n\n有时这个错误是因为读卡器出了故障。',
cannotWrite:
'看起来 Etcher 无法写入磁盘的这个位置。 此错误通常是由故障的磁盘、读取器或端口引起的。 \n\n请使用其他磁盘、读卡器或端口重试。',
childWriterDied:
'写入进程意外崩溃。请再试一次,如果问题仍然存在,请联系 Etcher 团队。',
badProtocol: '仅支持 http:// 和 https:// 开头的网址。',
},
target: {
selectTarget: '选择目标磁盘',
plugTarget: '请插入目标磁盘',
targets: '个目标',
change: '更改',
},
menu: {
edit: '编辑',
view: '视图',
devTool: '打开开发者工具',
window: '窗口',
help: '帮助',
pro: 'Etcher 专业版',
website: 'Etcher 的官网',
issue: '提交一个 issue',
about: '关于 Etcher',
hide: '隐藏 Etcher',
hideOthers: '隐藏其它窗口',
unhide: '取消隐藏',
quit: '退出 Etcher',
},
source: {
useSourceURL: '使用镜像网络地址',
auth: '验证',
username: '输入用户名',
password: '输入密码',
unsupportedProtocol: '不支持的协议',
windowsImage: '这可能是 Windows 系统镜像',
partitionTable: '找不到分区表',
errorOpen: '打开源镜像时出错',
fromFile: '从文件烧录',
fromURL: '从在线地址烧录',
clone: '克隆磁盘',
image: '镜像信息',
name: '名称:',
path: '路径:',
selectSource: '选择源',
plugSource: '请插入源磁盘',
osImages: '系统镜像格式',
allFiles: '任何文件格式',
enterValidURL: '请输入一个正确的地址',
},
drives: {
name: '名称',
size: '大小',
location: '位置',
find: '找到 {{length}} 个',
select: '选定 {{select}}',
showHidden: '显示 {{num}} 个隐藏的磁盘',
systemDriveDanger: '选择系统盘很危险,因为这将会删除你的系统!',
openInBrowser: 'Etcher 会在浏览器中打开 {{link}}',
changeTarget: '改变目标',
largeDriveWarning: '您即将擦除一个非常大的磁盘',
largeDriveWarningMsg: '您确定所选磁盘不是存储磁盘吗?',
systemDriveWarning: '您将要擦除系统盘',
systemDriveWarningMsg: '您确定要烧录到系统盘吗?',
},
flash: {
another: '烧录另一目标',
target: '目标',
location: '位置',
error: '错误',
flash: '烧录',
flashNow: '现在烧录!',
skip: '跳过了验证',
moreInfo: '更多信息',
speedTip:
'通过将镜像大小除以烧录时间来计算速度。\n由于我们能够跳过未使用的部分因此具有EXT分区的磁盘镜像烧录速度更快。',
speed: '速度:{{speed}} MB/秒',
speedShort: '{{speed}} MB/秒',
eta: '预计还需要:{{eta}}',
failedTarget: '失败的烧录目标',
failedRetry: '重试烧录失败目标',
flashFailed: '烧录失败。',
flashCompleted: '烧录成功!',
},
settings: {
errorReporting: '匿名地向 balena.io 报告运行错误和使用统计',
autoUpdate: '自动更新',
settings: '软件设置',
systemInformation: '系统信息',
},
},
};
export default translation;

152
lib/gui/app/i18n/zh-TW.ts Normal file
View File

@ -0,0 +1,152 @@
const translation = {
translation: {
ok: '好',
cancel: '取消',
continue: '繼續',
skip: '跳過',
sure: '我確定',
warning: '請注意!',
attention: '請注意',
failed: '失敗',
completed: '完畢',
yesExit: '是的,可以退出',
reallyExit: '真的要現在退出 Etcher 嗎?',
yesContinue: '是的,繼續',
progress: {
starting: '正在啓動……',
decompressing: '正在解壓……',
flashing: '正在燒錄……',
finishing: '正在結束……',
verifying: '正在驗證……',
failing: '失敗……',
},
message: {
sizeNotRecommended: '大小不推薦',
tooSmall: '空間太小',
locked: '被鎖定',
system: '系統盤',
containsImage: '存放源鏡像',
largeDrive: '很大的磁盤',
sourceLarger: '所選的鏡像比目標盤大了 {{byte}} 比特。',
flashSucceed_one: '燒錄成功',
flashSucceed_other: '燒錄成功',
flashFail_one: '燒錄失敗',
flashFail_other: '燒錄失敗',
toDrive: '到 {{description}} ({{name}})',
toTarget_one: '到 {{num}} 個目標',
toTarget_other: '到 {{num}} 個目標',
andFailTarget_one: '並燒錄失敗了 {{num}} 個目標',
andFailTarget_other: '並燒錄失敗了 {{num}} 個目標',
succeedTo: '{{name}} 被成功燒錄 {{target}}',
exitWhileFlashing:
'您當前正在刷機。 關閉 Etcher 可能會導致您的磁盤無法使用。',
looksLikeWindowsImage:
'看起來您正在嘗試刻錄 Windows 鏡像。\n\n與其他鏡像不同Windows 鏡像需要特殊處理才能使其可啓動。 我們建議您使用專門爲此目的設計的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
image: '鏡像',
drive: '磁盤',
missingPartitionTable:
'看起來這不是一個可啓動的{{type}}。\n\n這個{{type}}似乎不包含分區表,因此您的設備可能無法識別或無法正確啓動。',
largeDriveSize: '這是個很大的磁盤!請檢查並確認它不包含對您很重要的信息',
systemDrive: '選擇系統盤很危險,因爲這將會刪除你的系統',
sourceDrive: '源鏡像位於這個分區中',
noSpace: '磁盤空間不足。 請插入另一個較大的磁盤並重試。',
genericFlashError:
'出了點問題。如果源鏡像曾被壓縮過,請檢查它是否已損壞。\n{{error}}',
validation:
'寫入已成功完成,但 Etcher 在從磁盤讀取鏡像時檢測到潛在的損壞問題。 \n\n請考慮將鏡像寫入其他磁盤。',
openError: '打開 {{source}} 時出錯。\n\n錯誤信息 {{error}}',
flashError: '燒錄 {{image}} {{targets}} 失敗。',
unplug:
'看起來 Etcher 失去了對磁盤的連接。 它是不是被意外拔掉了?\n\n有時這個錯誤是因爲讀卡器出了故障。',
cannotWrite:
'看起來 Etcher 無法寫入磁盤的這個位置。 此錯誤通常是由故障的磁盤、讀取器或端口引起的。 \n\n請使用其他磁盤、讀卡器或端口重試。',
childWriterDied:
'寫入進程意外崩潰。請再試一次,如果問題仍然存在,請聯繫 Etcher 團隊。',
badProtocol: '僅支持 http:// 和 https:// 開頭的網址。',
},
target: {
selectTarget: '選擇目標磁盤',
plugTarget: '請插入目標磁盤',
targets: '個目標',
change: '更改',
},
menu: {
edit: '編輯',
view: '視圖',
devTool: '打開開發者工具',
window: '窗口',
help: '幫助',
pro: 'Etcher 專業版',
website: 'Etcher 的官網',
issue: '提交一個 issue',
about: '關於 Etcher',
hide: '隱藏 Etcher',
hideOthers: '隱藏其它窗口',
unhide: '取消隱藏',
quit: '退出 Etcher',
},
source: {
useSourceURL: '使用鏡像網絡地址',
auth: '驗證',
username: '輸入用戶名',
password: '輸入密碼',
unsupportedProtocol: '不支持的協議',
windowsImage: '這可能是 Windows 系統鏡像',
partitionTable: '找不到分區表',
errorOpen: '打開源鏡像時出錯',
fromFile: '從文件燒錄',
fromURL: '從在線地址燒錄',
clone: '克隆磁盤',
image: '鏡像信息',
name: '名稱:',
path: '路徑:',
selectSource: '選擇源',
plugSource: '請插入源磁盤',
osImages: '系統鏡像格式',
allFiles: '任何文件格式',
enterValidURL: '請輸入一個正確的地址',
},
drives: {
name: '名稱',
size: '大小',
location: '位置',
find: '找到 {{length}} 個',
select: '選定 {{select}}',
showHidden: '顯示 {{num}} 個隱藏的磁盤',
systemDriveDanger: '選擇系統盤很危險,因爲這將會刪除你的系統!',
openInBrowser: 'Etcher 會在瀏覽器中打開 {{link}}',
changeTarget: '改變目標',
largeDriveWarning: '您即將擦除一個非常大的磁盤',
largeDriveWarningMsg: '您確定所選磁盤不是存儲磁盤嗎?',
systemDriveWarning: '您將要擦除系統盤',
systemDriveWarningMsg: '您確定要燒錄到系統盤嗎?',
},
flash: {
another: '燒錄另一目標',
target: '目標',
location: '位置',
error: '錯誤',
flash: '燒錄',
flashNow: '現在燒錄!',
skip: '跳過了驗證',
moreInfo: '更多信息',
speedTip:
'通過將鏡像大小除以燒錄時間來計算速度。\n由於我們能夠跳過未使用的部分因此具有EXT分區的磁盤鏡像燒錄速度更快。',
speed: '速度:{{speed}} MB/秒',
speedShort: '{{speed}} MB/秒',
eta: '預計還需要:{{eta}}',
failedTarget: '失敗的燒錄目標',
failedRetry: '重試燒錄失敗目標',
flashFailed: '燒錄失敗。',
flashCompleted: '燒錄成功!',
},
settings: {
errorReporting: '匿名地向 balena.io 報告運行錯誤和使用統計',
autoUpdate: '自動更新',
settings: '軟件設置',
systemInformation: '系統信息',
},
},
};
export default translation;

View File

@ -15,6 +15,7 @@
*/ */
import * as prettyBytes from 'pretty-bytes'; import * as prettyBytes from 'pretty-bytes';
import * as i18next from 'i18next';
export interface FlashState { export interface FlashState {
active: number; active: number;
@ -34,36 +35,45 @@ export function fromFlashState({
position?: string; position?: string;
} { } {
if (type === undefined) { if (type === undefined) {
return { status: 'Starting...' }; return { status: i18next.t('progress.starting') };
} else if (type === 'decompressing') { } else if (type === 'decompressing') {
if (percentage == null) { if (percentage == null) {
return { status: 'Decompressing...' }; return { status: i18next.t('progress.decompressing') };
} else { } else {
return { position: `${percentage}%`, status: 'Decompressing...' }; return {
position: `${percentage}%`,
status: i18next.t('progress.decompressing'),
};
} }
} else if (type === 'flashing') { } else if (type === 'flashing') {
if (percentage != null) { if (percentage != null) {
if (percentage < 100) { if (percentage < 100) {
return { position: `${percentage}%`, status: 'Flashing...' }; return {
position: `${percentage}%`,
status: i18next.t('progress.flashing'),
};
} else { } else {
return { status: 'Finishing...' }; return { status: i18next.t('progress.finishing') };
} }
} else { } else {
return { return {
status: 'Flashing...', status: i18next.t('progress.flashing'),
position: `${position ? prettyBytes(position) : ''}`, position: `${position ? prettyBytes(position) : ''}`,
}; };
} }
} else if (type === 'verifying') { } else if (type === 'verifying') {
if (percentage == null) { if (percentage == null) {
return { status: 'Validating...' }; return { status: i18next.t('progress.verifying') };
} else if (percentage < 100) { } else if (percentage < 100) {
return { position: `${percentage}%`, status: 'Validating...' }; return {
position: `${percentage}%`,
status: i18next.t('progress.verifying'),
};
} else { } else {
return { status: 'Finishing...' }; return { status: i18next.t('progress.finishing') };
} }
} }
return { status: 'Failed' }; return { status: i18next.t('progress.failing') };
} }
export function titleFromFlashState( export function titleFromFlashState(

View File

@ -20,6 +20,7 @@ import * as _ from 'lodash';
import * as errors from '../../../shared/errors'; import * as errors from '../../../shared/errors';
import * as settings from '../../../gui/app/models/settings'; import * as settings from '../../../gui/app/models/settings';
import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats'; import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
import * as i18next from 'i18next';
async function mountSourceDrive() { async function mountSourceDrive() {
// sourceDrivePath is the name of the link in /dev/disk/by-path // sourceDrivePath is the name of the link in /dev/disk/by-path
@ -53,11 +54,11 @@ export async function selectImage(): Promise<string | undefined> {
properties: ['openFile', 'treatPackageAsDirectory'], properties: ['openFile', 'treatPackageAsDirectory'],
filters: [ filters: [
{ {
name: 'OS Images', name: i18next.t('source.osImages'),
extensions: SUPPORTED_EXTENSIONS, extensions: SUPPORTED_EXTENSIONS,
}, },
{ {
name: 'All', name: i18next.t('source.allFiles'),
extensions: ['*'], extensions: ['*'],
}, },
], ],
@ -79,8 +80,8 @@ export async function showWarning(options: {
description: string; description: string;
}): Promise<boolean> { }): Promise<boolean> {
_.defaults(options, { _.defaults(options, {
confirmationLabel: 'OK', confirmationLabel: i18next.t('ok'),
rejectionLabel: 'Cancel', rejectionLabel: i18next.t('cancel'),
}); });
const BUTTONS = [options.confirmationLabel, options.rejectionLabel]; const BUTTONS = [options.confirmationLabel, options.rejectionLabel];
@ -98,7 +99,7 @@ export async function showWarning(options: {
buttons: BUTTONS, buttons: BUTTONS,
defaultId: BUTTON_REJECTION_INDEX, defaultId: BUTTON_REJECTION_INDEX,
cancelId: BUTTON_REJECTION_INDEX, cancelId: BUTTON_REJECTION_INDEX,
title: 'Attention', title: i18next.t('attention'),
message: options.title, message: options.title,
detail: options.description, detail: options.description,
}, },

View File

@ -37,6 +37,7 @@ import {
import FlashSvg from '../../../assets/flash.svg'; import FlashSvg from '../../../assets/flash.svg';
import DriveStatusWarningModal from '../../components/drive-status-warning-modal/drive-status-warning-modal'; import DriveStatusWarningModal from '../../components/drive-status-warning-modal/drive-status-warning-modal';
import * as i18next from 'i18next';
const COMPLETED_PERCENTAGE = 100; const COMPLETED_PERCENTAGE = 100;
const SPEED_PRECISION = 2; const SPEED_PRECISION = 2;
@ -293,9 +294,17 @@ export class FlashStep extends React.PureComponent<
color="#7e8085" color="#7e8085"
width="100%" width="100%"
> >
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt> <Txt>
{i18next.t('flash.speedShort', {
speed: this.props.speed.toFixed(SPEED_PRECISION),
})}
</Txt>
{!_.isNil(this.props.eta) && ( {!_.isNil(this.props.eta) && (
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt> <Txt>
{i18next.t('flash.eta', {
eta: formatSeconds(this.props.eta),
})}
</Txt>
)} )}
</Flex> </Flex>
)} )}

View File

@ -1,5 +1,10 @@
// @ts-nocheck // @ts-nocheck
import { main } from './app'; import { main } from './app';
import './i18n';
import { langParser } from './i18n';
import { ipcRenderer } from 'electron';
ipcRenderer.send('change-lng', langParser());
if (module.hot) { if (module.hot) {
module.hot.accept('./app', () => { module.hot.accept('./app', () => {

View File

@ -142,7 +142,7 @@ export const Modal = styled(({ style, children, ...props }) => {
{...props} {...props}
> >
<ScrollableFlex flexDirection="column" width="100%" height="90%"> <ScrollableFlex flexDirection="column" width="100%" height="90%">
{...children} {children.length ? children.map((c: any) => <>{c}</>) : children}
</ScrollableFlex> </ScrollableFlex>
</ModalBase> </ModalBase>
); );

View File

@ -21,18 +21,22 @@ import { platform } from 'os';
import * as path from 'path'; import * as path from 'path';
import * as semver from 'semver'; import * as semver from 'semver';
import './app/i18n';
import { packageType, version } from '../../package.json'; import { packageType, version } from '../../package.json';
import * as EXIT_CODES from '../shared/exit-codes'; import * as EXIT_CODES from '../shared/exit-codes';
import { delay, getConfig } from '../shared/utils'; import { delay, getConfig } from '../shared/utils';
import * as settings from './app/models/settings'; import * as settings from './app/models/settings';
import { logException } from './app/modules/analytics'; import { logException } from './app/modules/analytics';
import { buildWindowMenu } from './menu'; import { buildWindowMenu } from './menu';
import * as i18n from 'i18next';
const customProtocol = 'etcher'; const customProtocol = 'etcher';
const scheme = `${customProtocol}://`; const scheme = `${customProtocol}://`;
const updatablePackageTypes = ['appimage', 'nsis', 'dmg']; const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
const packageUpdatable = updatablePackageTypes.includes(packageType); const packageUpdatable = updatablePackageTypes.includes(packageType);
let packageUpdated = false; let packageUpdated = false;
let mainWindow: any = null;
async function checkForUpdates(interval: number) { async function checkForUpdates(interval: number) {
// We use a while loop instead of a setInterval to preserve // We use a while loop instead of a setInterval to preserve
@ -130,7 +134,7 @@ async function createMainWindow() {
if (fullscreen) { if (fullscreen) {
({ width, height } = electron.screen.getPrimaryDisplay().bounds); ({ width, height } = electron.screen.getPrimaryDisplay().bounds);
} }
const mainWindow = new electron.BrowserWindow({ mainWindow = new electron.BrowserWindow({
width, width,
height, height,
frame: !fullscreen, frame: !fullscreen,
@ -157,7 +161,6 @@ async function createMainWindow() {
electron.app.setAsDefaultProtocolClient(customProtocol); electron.app.setAsDefaultProtocolClient(customProtocol);
buildWindowMenu(mainWindow);
mainWindow.setFullScreen(true); mainWindow.setFullScreen(true);
// Prevent flash of white when starting the application // Prevent flash of white when starting the application
@ -240,6 +243,17 @@ async function main(): Promise<void> {
await selectImageURL(await getCommandLineURL(argv)); await selectImageURL(await getCommandLineURL(argv));
}); });
await selectImageURL(await getCommandLineURL(process.argv)); await selectImageURL(await getCommandLineURL(process.argv));
electron.ipcMain.on('change-lng', function (event, args) {
i18n.changeLanguage(args, () => {
console.log('Language changed to: ' + args);
});
if (mainWindow != null) {
buildWindowMenu(mainWindow);
} else {
console.log('Build menu failed. ');
}
});
} }
} }

View File

@ -17,6 +17,8 @@
import * as electron from 'electron'; import * as electron from 'electron';
import { displayName } from '../../package.json'; import { displayName } from '../../package.json';
import * as i18next from 'i18next';
/** /**
* @summary Builds a native application menu for a given window * @summary Builds a native application menu for a given window
*/ */
@ -42,12 +44,13 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
const menuTemplate: electron.MenuItemConstructorOptions[] = [ const menuTemplate: electron.MenuItemConstructorOptions[] = [
{ {
role: 'editMenu', role: 'editMenu',
label: i18next.t('menu.edit'),
}, },
{ {
label: 'View', label: i18next.t('menu.view'),
submenu: [ submenu: [
{ {
label: 'Toggle Developer Tools', label: i18next.t('menu.devTool'),
accelerator: accelerator:
process.platform === 'darwin' ? 'Command+Alt+I' : 'Control+Shift+I', process.platform === 'darwin' ? 'Command+Alt+I' : 'Control+Shift+I',
click: toggleDevTools, click: toggleDevTools,
@ -56,12 +59,14 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
}, },
{ {
role: 'windowMenu', role: 'windowMenu',
label: i18next.t('menu.window'),
}, },
{ {
role: 'help', role: 'help',
label: i18next.t('menu.help'),
submenu: [ submenu: [
{ {
label: 'Etcher Pro', label: i18next.t('menu.pro'),
click() { click() {
electron.shell.openExternal( electron.shell.openExternal(
'https://etcher.io/pro?utm_source=etcher_menu&ref=etcher_menu', 'https://etcher.io/pro?utm_source=etcher_menu&ref=etcher_menu',
@ -69,13 +74,13 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
}, },
}, },
{ {
label: 'Etcher Website', label: i18next.t('menu.website'),
click() { click() {
electron.shell.openExternal('https://etcher.io?ref=etcher_menu'); electron.shell.openExternal('https://etcher.io?ref=etcher_menu');
}, },
}, },
{ {
label: 'Report an issue', label: i18next.t('menu.issue'),
click() { click() {
electron.shell.openExternal( electron.shell.openExternal(
'https://github.com/balena-io/etcher/issues', 'https://github.com/balena-io/etcher/issues',
@ -92,25 +97,29 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
submenu: [ submenu: [
{ {
role: 'about' as const, role: 'about' as const,
label: 'About Etcher', label: i18next.t('menu.about'),
}, },
{ {
type: 'separator' as const, type: 'separator' as const,
}, },
{ {
role: 'hide' as const, role: 'hide' as const,
label: i18next.t('menu.hide'),
}, },
{ {
role: 'hideOthers' as const, role: 'hideOthers' as const,
label: i18next.t('menu.hideOthers'),
}, },
{ {
role: 'unhide' as const, role: 'unhide' as const,
label: i18next.t('menu.unhide'),
}, },
{ {
type: 'separator' as const, type: 'separator' as const,
}, },
{ {
role: 'quit' as const, role: 'quit' as const,
label: i18next.t('menu.quit'),
}, },
], ],
}); });

View File

@ -0,0 +1,21 @@
#!/usr/bin/env osascript -l JavaScript
ObjC.import('stdlib')
const app = Application.currentApplication()
app.includeStandardAdditions = true
const result = app.displayDialog('balenaEtcher 需要来自管理员的权限才能烧录镜像到磁盘。\n\n输入您的密码以允许此操作。', {
defaultAnswer: '',
withIcon: 'caution',
buttons: ['取消', '好'],
defaultButton: '好',
hiddenAnswer: true,
})
if (result.buttonReturned === '好') {
result.textReturned
} else {
$.exit(255)
}

View File

@ -30,6 +30,9 @@ export async function sudo(
command: string, command: string,
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> { ): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
try { try {
let lang = Intl.DateTimeFormat().resolvedOptions().locale;
lang = lang.substr(0, 2);
const { stdout, stderr } = await execFileAsync( const { stdout, stderr } = await execFileAsync(
'sudo', 'sudo',
['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`], ['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`],
@ -40,7 +43,7 @@ export async function sudo(
SUDO_ASKPASS: join( SUDO_ASKPASS: join(
getAppPath(), getAppPath(),
__dirname, __dirname,
'sudo-askpass.osascript.js', 'sudo-askpass.osascript-' + lang + '.js',
), ),
}, },
}, },

View File

@ -17,16 +17,16 @@
import { Dictionary } from 'lodash'; import { Dictionary } from 'lodash';
import { outdent } from 'outdent'; import { outdent } from 'outdent';
import * as prettyBytes from 'pretty-bytes'; import * as prettyBytes from 'pretty-bytes';
import '../gui/app/i18n';
import * as i18next from 'i18next';
export const progress: Dictionary<(quantity: number) => string> = { export const progress: Dictionary<(quantity: number) => string> = {
successful: (quantity: number) => { successful: (quantity: number) => {
const plural = quantity === 1 ? '' : 's'; return i18next.t('message.flashSucceed', { count: quantity });
return `Successful target${plural}`;
}, },
failed: (quantity: number) => { failed: (quantity: number) => {
const plural = quantity === 1 ? '' : 's'; return i18next.t('message.flashFail', { count: quantity });
return `Failed target${plural}`;
}, },
}; };
@ -38,129 +38,121 @@ export const info = {
) => { ) => {
const targets = []; const targets = [];
if (failed + successful === 1) { if (failed + successful === 1) {
targets.push(`to ${drive.description} (${drive.displayName})`); targets.push(
i18next.t('message.toDrive', {
description: drive.description,
name: drive.displayName,
}),
);
} else { } else {
if (successful) { if (successful) {
const plural = successful === 1 ? '' : 's'; targets.push(
targets.push(`to ${successful} target${plural}`); i18next.t('message.toTarget', {
count: successful,
num: successful,
}),
);
} }
if (failed) { if (failed) {
const plural = failed === 1 ? '' : 's'; targets.push(
targets.push(`and failed to be flashed to ${failed} target${plural}`); i18next.t('message.andFailTarget', { count: failed, num: failed }),
);
} }
} }
return `${imageBasename} was successfully flashed ${targets.join(' ')}`; return i18next.t('message.succeedTo', {
name: imageBasename,
target: targets.join(' '),
});
}, },
}; };
export const compatibility = { export const compatibility = {
sizeNotRecommended: () => { sizeNotRecommended: () => {
return 'Not recommended'; return i18next.t('message.sizeNotRecommended');
}, },
tooSmall: () => { tooSmall: () => {
return 'Too small'; return i18next.t('message.tooSmall');
}, },
locked: () => { locked: () => {
return 'Locked'; return i18next.t('message.locked');
}, },
system: () => { system: () => {
return 'System drive'; return i18next.t('message.system');
}, },
containsImage: () => { containsImage: () => {
return 'Source drive'; return i18next.t('message.containsImage');
}, },
// The drive is large and therefore likely not a medium you want to write to. // The drive is large and therefore likely not a medium you want to write to.
largeDrive: () => { largeDrive: () => {
return 'Large drive'; return i18next.t('message.largeDrive');
}, },
} as const; } as const;
export const warning = { export const warning = {
tooSmall: (source: { size: number }, target: { size: number }) => { tooSmall: (source: { size: number }, target: { size: number }) => {
return outdent({ newline: ' ' })` return outdent({ newline: ' ' })`
The selected source is ${prettyBytes(source.size - target.size)} ${i18next.t('message.sourceLarger', {
larger than this drive. byte: prettyBytes(source.size - target.size),
})}
`; `;
}, },
exitWhileFlashing: () => { exitWhileFlashing: () => {
return [ return i18next.t('message.exitWhileFlashing');
'You are currently flashing a drive.',
'Closing Etcher may leave your drive in an unusable state.',
].join(' ');
}, },
looksLikeWindowsImage: () => { looksLikeWindowsImage: () => {
return [ return i18next.t('message.looksLikeWindowsImage');
'It looks like you are trying to burn a Windows image.\n\n',
'Unlike other images, Windows images require special processing to be made bootable.',
'We suggest you use a tool specially designed for this purpose, such as',
'<a href="https://rufus.akeo.ie">Rufus</a> (Windows),',
'<a href="https://github.com/slacka/WoeUSB">WoeUSB</a> (Linux),',
'or Boot Camp Assistant (macOS).',
].join(' ');
}, },
missingPartitionTable: () => { missingPartitionTable: () => {
return [ return i18next.t('message.missingPartitionTable', {
'It looks like this is not a bootable image.\n\n', type: i18next.t('message.image'),
'The image does not appear to contain a partition table,', });
'and might not be recognized or bootable by your device.',
].join(' ');
}, },
driveMissingPartitionTable: () => { driveMissingPartitionTable: () => {
return outdent({ newline: ' ' })` return i18next.t('message.missingPartitionTable', {
It looks like this is not a bootable drive. type: i18next.t('message.drive'),
The drive does not appear to contain a partition table, });
and might not be recognized or bootable by your device.
`;
}, },
largeDriveSize: () => { largeDriveSize: () => {
return "This is a large drive! Make sure it doesn't contain files that you want to keep."; return i18next.t('message.largeDriveSize');
}, },
systemDrive: () => { systemDrive: () => {
return 'Selecting your system drive is dangerous and will erase your drive!'; return i18next.t('message.systemDrive');
}, },
sourceDrive: () => { sourceDrive: () => {
return 'Contains the image you chose to flash'; return i18next.t('message.sourceDrive');
}, },
}; };
export const error = { export const error = {
notEnoughSpaceInDrive: () => { notEnoughSpaceInDrive: () => {
return [ return i18next.t('message.noSpace');
'Not enough space on the drive.',
'Please insert larger one and try again.',
].join(' ');
}, },
genericFlashError: (err: Error) => { genericFlashError: (err: Error) => {
return `Something went wrong. If it is a compressed image, please check that the archive is not corrupted.\n${err.message}`; return i18next.t('message.genericFlashError', { error: err.message });
}, },
validation: () => { validation: () => {
return [ return i18next.t('message.validation');
'The write has been completed successfully but Etcher detected potential',
'corruption issues when reading the image back from the drive.',
'\n\nPlease consider writing the image to a different drive.',
].join(' ');
}, },
openSource: (sourceName: string, errorMessage: string) => { openSource: (sourceName: string, errorMessage: string) => {
return outdent` return i18next.t('message.openError', {
Something went wrong while opening ${sourceName} source: sourceName,
error: errorMessage,
Error: ${errorMessage} });
`;
}, },
flashFailure: ( flashFailure: (
@ -169,35 +161,33 @@ export const error = {
) => { ) => {
const target = const target =
drives.length === 1 drives.length === 1
? `${drives[0].description} (${drives[0].displayName})` ? i18next.t('message.toDrive', {
: `${drives.length} targets`; description: drives[0].description,
return `Something went wrong while writing ${imageBasename} to ${target}.`; name: drives[0].displayName,
})
: i18next.t('message.toTarget', {
count: drives.length,
num: drives.length,
});
return i18next.t('message.flashError', {
image: imageBasename,
targets: target,
});
}, },
driveUnplugged: () => { driveUnplugged: () => {
return [ return i18next.t('message.unplug');
'Looks like Etcher lost access to the drive.',
'Did it get unplugged accidentally?',
"\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.",
].join(' ');
}, },
inputOutput: () => { inputOutput: () => {
return [ return i18next.t('message.cannotWrite');
'Looks like Etcher is not able to write to this location of the drive.',
'This error is usually caused by a faulty drive, reader, or port.',
'\n\nPlease try again with another drive, reader, or port.',
].join(' ');
}, },
childWriterDied: () => { childWriterDied: () => {
return [ return i18next.t('message.childWriterDied');
'The writer process ended unexpectedly.',
'Please try again, and contact the Etcher team if the problem persists.',
].join(' ');
}, },
unsupportedProtocol: () => { unsupportedProtocol: () => {
return 'Only http:// and https:// URLs are supported.'; return i18next.t('message.badProtocol');
}, },
}; };

119
package-lock.json generated
View File

@ -4495,7 +4495,7 @@
"asap": { "asap": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
"dev": true "dev": true
}, },
"asar": { "asar": {
@ -4658,7 +4658,7 @@
"babel-plugin-syntax-jsx": { "babel-plugin-syntax-jsx": {
"version": "6.18.0", "version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=",
"dev": true "dev": true
}, },
"bail": { "bail": {
@ -5401,7 +5401,7 @@
"change-emitter": { "change-emitter": {
"version": "0.1.6", "version": "0.1.6",
"resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz", "resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz",
"integrity": "sha512-YXzt1cQ4a2jqazhcuSWEOc1K2q8g9H6eWNsyZgi640LDzRWVQ2eDe+Y/kVdftH+vYdPF2rgDb3dLdpxE1jvAxw==", "integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=",
"dev": true "dev": true
}, },
"character-entities": { "character-entities": {
@ -5437,7 +5437,7 @@
"check-error": { "check-error": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true "dev": true
}, },
"chokidar": { "chokidar": {
@ -5600,7 +5600,7 @@
"codemirror-spell-checker": { "codemirror-spell-checker": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz", "resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz",
"integrity": "sha512-2Tl6n0v+GJRsC9K3MLCdLaMOmvWL0uukajNJseorZJsslaxZyZMgENocPU8R0DyoTAiKsyqiemSOZo7kjGV0LQ==", "integrity": "sha1-HGYPkIlIPMtRE7m6nKGcP0mTNx4=",
"dev": true, "dev": true,
"requires": { "requires": {
"typo-js": "*" "typo-js": "*"
@ -6083,7 +6083,7 @@
"css-color-keywords": { "css-color-keywords": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=",
"dev": true "dev": true
}, },
"css-loader": { "css-loader": {
@ -6702,7 +6702,7 @@
"dedent": { "dedent": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
"integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
"dev": true "dev": true
}, },
"deep-eql": { "deep-eql": {
@ -9107,7 +9107,7 @@
"core-js": { "core-js": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
"integrity": "sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==", "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=",
"dev": true "dev": true
}, },
"promise": { "promise": {
@ -9422,7 +9422,7 @@
"get-func-name": { "get-func-name": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"dev": true "dev": true
}, },
"get-intrinsic": { "get-intrinsic": {
@ -9888,6 +9888,15 @@
"integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==", "integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==",
"dev": true "dev": true
}, },
"html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dev": true,
"requires": {
"void-elements": "3.1.0"
}
},
"html-void-elements": { "html-void-elements": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz",
@ -10108,6 +10117,32 @@
} }
} }
}, },
"i18next": {
"version": "21.8.14",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.14.tgz",
"integrity": "sha512-4Yi+DtexvMm/Yw3Q9fllzY12SgLk+Mcmar+rCAccsOPul/2UmnBzoHbTGn/L48IPkFcmrNaH7xTLboBWIbH6pw==",
"dev": true,
"requires": {
"@babel/runtime": "^7.17.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz",
"integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.11"
}
},
"regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"dev": true
}
}
},
"iconv-corefoundation": { "iconv-corefoundation": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
@ -10115,7 +10150,8 @@
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
"cli-truncate": "^2.1.0" "cli-truncate": "^2.1.0",
"node-addon-api": "^1.6.3"
} }
}, },
"iconv-lite": { "iconv-lite": {
@ -10615,7 +10651,7 @@
"is-regexp": { "is-regexp": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
"integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
"dev": true "dev": true
}, },
"is-shared-array-buffer": { "is-shared-array-buffer": {
@ -10723,7 +10759,7 @@
"isomorphic-fetch": { "isomorphic-fetch": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
"integrity": "sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA==", "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
"dev": true, "dev": true,
"requires": { "requires": {
"node-fetch": "^1.0.1", "node-fetch": "^1.0.1",
@ -10920,7 +10956,7 @@
"json-stable-stringify-without-jsonify": { "json-stable-stringify-without-jsonify": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
"dev": true "dev": true
}, },
"json-stringify-safe": { "json-stringify-safe": {
@ -11231,7 +11267,7 @@
"lodash.get": { "lodash.get": {
"version": "4.4.2", "version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
"dev": true "dev": true
}, },
"lodash.isequal": { "lodash.isequal": {
@ -11655,7 +11691,7 @@
"mdurl": { "mdurl": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=",
"dev": true "dev": true
}, },
"media-typer": { "media-typer": {
@ -12701,6 +12737,13 @@
} }
} }
}, },
"node-addon-api": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
"integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
"dev": true,
"optional": true
},
"node-api-version": { "node-api-version": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.1.4.tgz", "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.1.4.tgz",
@ -12734,7 +12777,7 @@
"is-stream": { "is-stream": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
"dev": true "dev": true
} }
} }
@ -13468,7 +13511,7 @@
"isarray": { "isarray": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true "dev": true
} }
} }
@ -13494,7 +13537,7 @@
"performance-now": { "performance-now": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
"dev": true "dev": true
}, },
"picocolors": { "picocolors": {
@ -13812,7 +13855,7 @@
"prr": { "prr": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
"dev": true "dev": true
}, },
"pseudomap": { "pseudomap": {
@ -14193,6 +14236,16 @@
"react-side-effect": "^2.1.0" "react-side-effect": "^2.1.0"
} }
}, },
"react-i18next": {
"version": "11.18.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.1.tgz",
"integrity": "sha512-S8cl4mvIOSA7OQCE5jNy2yhv705Vwi+7PinpqKIYcBmX/trJtHKqrf6CL67WJSA8crr2JU+oxE9jn9DQIrQezg==",
"dev": true,
"requires": {
"@babel/runtime": "^7.14.5",
"html-parse-stringify": "^3.0.1"
}
},
"react-is": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -14651,7 +14704,7 @@
"repeat-string": { "repeat-string": {
"version": "1.6.1", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
"integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
"dev": true "dev": true
}, },
"require-directory": { "require-directory": {
@ -15196,7 +15249,7 @@
"setimmediate": { "setimmediate": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
"dev": true "dev": true
}, },
"setprototypeof": { "setprototypeof": {
@ -15431,7 +15484,7 @@
"simple-swizzle": { "simple-swizzle": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
"dev": true, "dev": true,
"requires": { "requires": {
"is-arrayish": "^0.3.1" "is-arrayish": "^0.3.1"
@ -15934,7 +15987,7 @@
"is-obj": { "is-obj": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true "dev": true
} }
} }
@ -16296,7 +16349,7 @@
"toggle-selection": { "toggle-selection": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=",
"dev": true "dev": true
}, },
"toidentifier": { "toidentifier": {
@ -16320,7 +16373,7 @@
"trim": { "trim": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
"integrity": "sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==", "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=",
"dev": true "dev": true
}, },
"trim-trailing-lines": { "trim-trailing-lines": {
@ -17009,19 +17062,19 @@
"validate.io-array": { "validate.io-array": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz",
"integrity": "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==", "integrity": "sha1-W1osr9j4uFq7L4hroVPy2Tond00=",
"dev": true "dev": true
}, },
"validate.io-function": { "validate.io-function": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz", "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz",
"integrity": "sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==", "integrity": "sha1-NDoZgC7TsZaCaceA5VjpNBHAutc=",
"dev": true "dev": true
}, },
"validate.io-integer": { "validate.io-integer": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz", "resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz",
"integrity": "sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==", "integrity": "sha1-FoSWSAuVviJH7EQ/IjPeT4mHgGg=",
"dev": true, "dev": true,
"requires": { "requires": {
"validate.io-number": "^1.0.3" "validate.io-number": "^1.0.3"
@ -17030,7 +17083,7 @@
"validate.io-integer-array": { "validate.io-integer-array": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz", "resolved": "https://registry.npmjs.org/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz",
"integrity": "sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==", "integrity": "sha1-LKveAzKTpry+Bj/q/pHq9GsToIk=",
"dev": true, "dev": true,
"requires": { "requires": {
"validate.io-array": "^1.0.3", "validate.io-array": "^1.0.3",
@ -17040,7 +17093,7 @@
"validate.io-number": { "validate.io-number": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz", "resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz",
"integrity": "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==", "integrity": "sha1-9j/+2iSL8opnqNSODjtGGhZluvg=",
"dev": true "dev": true
}, },
"vary": { "vary": {
@ -17098,6 +17151,12 @@
"unist-util-stringify-position": "^2.0.0" "unist-util-stringify-position": "^2.0.0"
} }
}, },
"void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"dev": true
},
"watchpack": { "watchpack": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",

View File

@ -84,6 +84,7 @@
"etcher-sdk": "7.4.0", "etcher-sdk": "7.4.0",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"husky": "4.3.8", "husky": "4.3.8",
"i18next": "21.8.14",
"immutable": "3.8.2", "immutable": "3.8.2",
"lint-staged": "10.5.4", "lint-staged": "10.5.4",
"lodash": "4.17.21", "lodash": "4.17.21",
@ -98,6 +99,7 @@
"pretty-bytes": "5.6.0", "pretty-bytes": "5.6.0",
"react": "16.8.5", "react": "16.8.5",
"react-dom": "16.8.5", "react-dom": "16.8.5",
"react-i18next": "11.18.1",
"redux": "4.2.0", "redux": "4.2.0",
"rendition": "19.3.2", "rendition": "19.3.2",
"resin-corvus": "2.0.5", "resin-corvus": "2.0.5",