mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-29 14:16:36 +00:00
feat: make i18n and add Chinese support
- make i18n using i18next - add Chinese (Simplified) support
This commit is contained in:
parent
4b786b8a9f
commit
db1bf7e488
@ -7,7 +7,8 @@ afterSign: ./afterSignHook.js
|
||||
asar: false
|
||||
files:
|
||||
- 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:
|
||||
icon: assets/icon.icns
|
||||
category: public.app-category.developer-tools
|
||||
|
@ -38,6 +38,7 @@ import * as osDialog from './os/dialog';
|
||||
import * as windowProgress from './os/window-progress';
|
||||
import MainPage from './pages/main/MainPage';
|
||||
import './css/main.css';
|
||||
import i18next from 'i18next';
|
||||
|
||||
window.addEventListener(
|
||||
'unhandledrejection',
|
||||
@ -313,9 +314,9 @@ window.addEventListener('beforeunload', async (event) => {
|
||||
|
||||
try {
|
||||
const confirmed = await osDialog.showWarning({
|
||||
confirmationLabel: 'Yes, quit',
|
||||
rejectionLabel: 'Cancel',
|
||||
title: 'Are you sure you want to close Etcher?',
|
||||
confirmationLabel: i18next.t('yesExit'),
|
||||
rejectionLabel: i18next.t('cancel'),
|
||||
title: i18next.t('reallyExit'),
|
||||
description: messages.warning.exitWhileFlashing(),
|
||||
});
|
||||
if (confirmed) {
|
||||
|
@ -44,6 +44,7 @@ import {
|
||||
|
||||
import { SourceMetadata } from '../source-selector/source-selector';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import i18next from 'i18next';
|
||||
|
||||
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
||||
progress: number;
|
||||
@ -189,7 +190,7 @@ export class DriveSelector extends React.Component<
|
||||
this.tableColumns = [
|
||||
{
|
||||
field: 'description',
|
||||
label: 'Name',
|
||||
label: i18next.t('drives.name'),
|
||||
render: (description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive)) {
|
||||
const isLargeDrive = isDriveSizeLarge(drive);
|
||||
@ -215,7 +216,7 @@ export class DriveSelector extends React.Component<
|
||||
{
|
||||
field: 'description',
|
||||
key: 'size',
|
||||
label: 'Size',
|
||||
label: i18next.t('drives.size'),
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||
return prettyBytes(drive.size);
|
||||
@ -225,7 +226,7 @@ export class DriveSelector extends React.Component<
|
||||
{
|
||||
field: 'description',
|
||||
key: 'link',
|
||||
label: 'Location',
|
||||
label: i18next.t('drives.location'),
|
||||
render: (_description: string, drive: Drive) => {
|
||||
return (
|
||||
<Txt>
|
||||
@ -399,14 +400,14 @@ export class DriveSelector extends React.Component<
|
||||
color="#5b82a7"
|
||||
style={{ fontWeight: 600 }}
|
||||
>
|
||||
{drives.length} found
|
||||
{i18next.t('drives.find', { length: drives.length })}
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
||||
cancel={() => cancel(this.originalList)}
|
||||
done={() => done(selectedList)}
|
||||
action={`Select (${selectedList.length})`}
|
||||
action={i18next.t('drives.select', { select: selectedList.length })}
|
||||
primaryButtonProps={{
|
||||
primary: !showWarnings,
|
||||
warning: showWarnings,
|
||||
@ -512,7 +513,11 @@ export class DriveSelector extends React.Component<
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
|
||||
<Txt ml={8}>
|
||||
{i18next.t('drives.showHidden', {
|
||||
num: numberOfHiddenSystemDrives,
|
||||
})}
|
||||
</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
@ -520,7 +525,7 @@ export class DriveSelector extends React.Component<
|
||||
)}
|
||||
{this.props.showWarnings && hasSystemDrives ? (
|
||||
<Alert className="system-drive-alert" style={{ width: '67%' }}>
|
||||
Selecting your system drive is dangerous and will erase your drive!
|
||||
{i18next.t('drives.systemDriveDanger')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
@ -540,13 +545,15 @@ export class DriveSelector extends React.Component<
|
||||
this.setState({ missingDriversModal: {} });
|
||||
}
|
||||
}}
|
||||
action="Yes, continue"
|
||||
action={i18next.t('yesContinue')}
|
||||
cancelButtonProps={{
|
||||
children: 'Cancel',
|
||||
children: i18next.t('cancel'),
|
||||
}}
|
||||
children={
|
||||
missingDriversModal.drive.linkMessage ||
|
||||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
|
||||
i18next.t('drives.openInBrowser', {
|
||||
link: missingDriversModal.drive.link,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
@ -7,6 +7,7 @@ import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { DriveWithWarnings } from '../../pages/main/Flash';
|
||||
import i18next from 'i18next';
|
||||
|
||||
const DriveStatusWarningModal = ({
|
||||
done,
|
||||
@ -17,12 +18,12 @@ const DriveStatusWarningModal = ({
|
||||
isSystem: boolean;
|
||||
drivesWithWarnings: DriveWithWarnings[];
|
||||
}) => {
|
||||
let warningSubtitle = 'You are about to erase an unusually large drive';
|
||||
let warningCta = 'Are you sure the selected drive is not a storage drive?';
|
||||
let warningSubtitle = i18next.t('drives.largeDriveWarning');
|
||||
let warningCta = i18next.t('drives.largeDriveWarningMsg');
|
||||
|
||||
if (isSystem) {
|
||||
warningSubtitle = "You are about to erase your computer's drives";
|
||||
warningCta = 'Are you sure you want to flash your system drive?';
|
||||
warningSubtitle = i18next.t('drives.systemDriveWarning');
|
||||
warningCta = i18next.t('drives.systemDriveWarningMsg');
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
@ -33,9 +34,9 @@ const DriveStatusWarningModal = ({
|
||||
cancelButtonProps={{
|
||||
primary: false,
|
||||
warning: true,
|
||||
children: 'Change target',
|
||||
children: i18next.t('drives.changeTarget'),
|
||||
}}
|
||||
action={"Yes, I'm sure"}
|
||||
action={i18next.t('sure')}
|
||||
primaryButtonProps={{
|
||||
primary: false,
|
||||
outline: true,
|
||||
@ -50,7 +51,7 @@ const DriveStatusWarningModal = ({
|
||||
<Flex flexDirection="column">
|
||||
<ExclamationTriangleSvg height="2em" fill="#fca321" />
|
||||
<Txt fontSize="24px" color="#fca321">
|
||||
WARNING!
|
||||
{i18next.t('warning')}
|
||||
</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize="24px">{warningSubtitle}</Txt>
|
||||
|
@ -17,6 +17,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { BaseButton } from '../../styled-components';
|
||||
import i18next from 'i18next';
|
||||
|
||||
export interface FlashAnotherProps {
|
||||
onClick: () => void;
|
||||
@ -25,7 +26,7 @@ export interface FlashAnotherProps {
|
||||
export const FlashAnother = (props: FlashAnotherProps) => {
|
||||
return (
|
||||
<BaseButton primary onClick={props.onClick}>
|
||||
Flash another
|
||||
{i18next.t('flash.another')}
|
||||
</BaseButton>
|
||||
);
|
||||
};
|
||||
|
@ -31,6 +31,7 @@ import { resetState } from '../../models/flash-state';
|
||||
import * as selection from '../../models/selection-state';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import { Modal, Table } from '../../styled-components';
|
||||
import i18next from 'i18next';
|
||||
|
||||
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
|
||||
&&& [data-display='table-head'],
|
||||
@ -88,15 +89,15 @@ function formattedErrors(errors: FlashError[]) {
|
||||
const columns: Array<TableColumn<FlashError>> = [
|
||||
{
|
||||
field: 'description',
|
||||
label: 'Target',
|
||||
label: i18next.t('flash.target'),
|
||||
},
|
||||
{
|
||||
field: 'device',
|
||||
label: 'Location',
|
||||
label: i18next.t('flash.location'),
|
||||
},
|
||||
{
|
||||
field: 'message',
|
||||
label: 'Error',
|
||||
label: i18next.t('flash.error'),
|
||||
render: (message: string, { code }: FlashError) => {
|
||||
return message ?? code;
|
||||
},
|
||||
@ -162,9 +163,10 @@ export function FlashResults({
|
||||
<Txt>{middleEllipsis(image, 24)}</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize={24} color="#fff" mb="17px">
|
||||
Flash {allFailed ? 'Failed' : 'Complete'}!
|
||||
{i18next.t('flash.flash')}{' '}
|
||||
{allFailed ? i18next.t('failed') : i18next.t('completed')}!
|
||||
</Txt>
|
||||
{skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null}
|
||||
{skip ? <Txt color="#7e8085">{i18next.t('flash.skip')}</Txt> : null}
|
||||
</Flex>
|
||||
<Flex flexDirection="column" color="#7e8085">
|
||||
{results.devices.successful !== 0 ? (
|
||||
@ -188,7 +190,7 @@ export function FlashResults({
|
||||
{progress.failed(errors.length)}
|
||||
</Txt>
|
||||
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
|
||||
more info
|
||||
{i18next.t('flash.moreInfo')}
|
||||
</Link>
|
||||
</Flex>
|
||||
) : null}
|
||||
@ -199,12 +201,9 @@ export function FlashResults({
|
||||
fontWeight: 500,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
tooltip={outdent({ newline: ' ' })`
|
||||
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.
|
||||
`}
|
||||
tooltip={i18next.t('flash.speedTip')}
|
||||
>
|
||||
Effective speed: {effectiveSpeed} MB/s
|
||||
{i18next.t('flash.speed')} {effectiveSpeed} MB/s
|
||||
</Txt>
|
||||
)}
|
||||
</Flex>
|
||||
@ -214,11 +213,11 @@ export function FlashResults({
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
Failed targets
|
||||
{i18next.t('failedTarget')}
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
action="Retry failed targets"
|
||||
action={i18next.t('failedRetry')}
|
||||
cancel={() => setShowErrorsInfo(false)}
|
||||
done={() => {
|
||||
setShowErrorsInfo(false);
|
||||
|
@ -20,6 +20,7 @@ import { default as styled } from 'styled-components';
|
||||
|
||||
import { fromFlashState } from '../../modules/progress-status';
|
||||
import { StepButton } from '../../styled-components';
|
||||
import i18next from 'i18next';
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar)`
|
||||
> div {
|
||||
@ -28,6 +29,7 @@ const FlashProgressBar = styled(ProgressBar)`
|
||||
color: white !important;
|
||||
text-shadow: none !important;
|
||||
transition-duration: 0s;
|
||||
|
||||
> div {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
@ -61,7 +63,7 @@ const colors = {
|
||||
} as const;
|
||||
|
||||
const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||
const status = type === 'verifying' ? 'Skip' : 'Cancel';
|
||||
const status = type === 'verifying' ? i18next.t('skip') : i18next.t('cancel');
|
||||
return (
|
||||
<Button plain onClick={() => onClick(status)} {...props}>
|
||||
{status}
|
||||
@ -69,6 +71,7 @@ const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||
);
|
||||
})`
|
||||
font-weight: 600;
|
||||
|
||||
&&& {
|
||||
width: auto;
|
||||
height: auto;
|
||||
@ -126,7 +129,7 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
marginTop: 30,
|
||||
}}
|
||||
>
|
||||
Flash!
|
||||
{i18next.t('flash.flashNow')}
|
||||
</StepButton>
|
||||
);
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import * as settings from '../../models/settings';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { Modal } from '../../styled-components';
|
||||
import i18next from 'i18next';
|
||||
|
||||
interface Setting {
|
||||
name: string;
|
||||
@ -34,13 +35,13 @@ async function getSettingsList(): Promise<Setting[]> {
|
||||
const list: Setting[] = [
|
||||
{
|
||||
name: 'errorReporting',
|
||||
label: 'Anonymously report errors and usage statistics to balena.io',
|
||||
label: i18next.t('settings.errorReporting'),
|
||||
},
|
||||
];
|
||||
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
|
||||
list.push({
|
||||
name: 'updatesEnabled',
|
||||
label: 'Auto-updates enabled',
|
||||
label: i18next.t('settings.autoUpdate'),
|
||||
});
|
||||
}
|
||||
return list;
|
||||
@ -58,6 +59,7 @@ const InfoBox = (props: any) => (
|
||||
<TextWithCopy code text={props.value} copy={props.value} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
|
||||
React.useEffect(() => {
|
||||
@ -92,7 +94,7 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
<Modal
|
||||
titleElement={
|
||||
<Txt fontSize={24} mb={24}>
|
||||
Settings
|
||||
{i18next.t('settings.settings')}
|
||||
</Txt>
|
||||
}
|
||||
done={() => toggleModal(false)}
|
||||
@ -113,7 +115,7 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
})}
|
||||
{UUID !== undefined && (
|
||||
<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)} />
|
||||
</Flex>
|
||||
)}
|
||||
|
@ -66,6 +66,7 @@ import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { isJson } from '../../../../shared/utils';
|
||||
import i18next from 'i18next';
|
||||
|
||||
const recentUrlImagesKey = 'recentUrlImages';
|
||||
|
||||
@ -160,7 +161,7 @@ const URLSelector = ({
|
||||
primaryButtonProps={{
|
||||
disabled: loading || !imageURL,
|
||||
}}
|
||||
action={loading ? <Spinner /> : 'OK'}
|
||||
action={loading ? <Spinner /> : i18next.t('ok')}
|
||||
done={async () => {
|
||||
setLoading(true);
|
||||
const urlStrings = recentImages.map((url: URL) => url.href);
|
||||
@ -176,11 +177,11 @@ const URLSelector = ({
|
||||
<Flex flexDirection="column">
|
||||
<Flex mb={15} style={{ width: '100%' }} flexDirection="column">
|
||||
<Txt mb="10px" fontSize="24px">
|
||||
Use Image URL
|
||||
{i18next.t('source.useSourceURL')}
|
||||
</Txt>
|
||||
<Input
|
||||
value={imageURL}
|
||||
placeholder="Enter a valid URL"
|
||||
placeholder={i18next.t('source.enterValidURL')}
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setImageURL(evt.target.value)
|
||||
@ -205,7 +206,7 @@ const URLSelector = ({
|
||||
{!showBasicAuth && (
|
||||
<ChevronRightSvg height="1em" fill="currentColor" />
|
||||
)}
|
||||
<Txt ml={8}>Authentication</Txt>
|
||||
<Txt ml={8}>{i18next.t('source.auth')}</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
{showBasicAuth && (
|
||||
@ -213,7 +214,7 @@ const URLSelector = ({
|
||||
<Input
|
||||
mb={15}
|
||||
value={username}
|
||||
placeholder="Enter username"
|
||||
placeholder={i18next.t('source.username')}
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setUsername(evt.target.value)
|
||||
@ -221,7 +222,7 @@ const URLSelector = ({
|
||||
/>
|
||||
<Input
|
||||
value={password}
|
||||
placeholder="Enter password"
|
||||
placeholder={i18next.t('source.password')}
|
||||
type="password"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setPassword(evt.target.value)
|
||||
@ -295,7 +296,7 @@ const FlowSelector = styled(
|
||||
font-weight: 600;
|
||||
|
||||
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))
|
||||
) {
|
||||
this.handleError(
|
||||
'Unsupported protocol',
|
||||
i18next.t('source.unsupportedProtocol'),
|
||||
selected,
|
||||
messages.error.unsupportedProtocol(),
|
||||
);
|
||||
@ -465,7 +466,7 @@ export class SourceSelector extends React.Component<
|
||||
this.setState({
|
||||
warning: {
|
||||
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({
|
||||
warning: {
|
||||
message: messages.warning.missingPartitionTable(),
|
||||
title: 'Missing partition table',
|
||||
title: i18next.t('source.partitionTable'),
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.handleError(
|
||||
'Error opening source',
|
||||
i18next.t('source.errorOpen'),
|
||||
sourcePath,
|
||||
messages.error.openSource(sourcePath, error.message),
|
||||
error,
|
||||
@ -515,7 +516,7 @@ export class SourceSelector extends React.Component<
|
||||
this.setState({
|
||||
warning: {
|
||||
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}
|
||||
onClick={() => this.reselectSource()}
|
||||
>
|
||||
Remove
|
||||
{i18next.t('cancel')}
|
||||
</ChangeButton>
|
||||
)}
|
||||
{!_.isNil(imageSize) && !imageLoading && (
|
||||
@ -734,7 +735,7 @@ export class SourceSelector extends React.Component<
|
||||
key="Flash from file"
|
||||
flow={{
|
||||
onClick: () => this.openImageSelector(),
|
||||
label: 'Flash from file',
|
||||
label: i18next.t('source.fromFile'),
|
||||
icon: <FileSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
@ -744,7 +745,7 @@ export class SourceSelector extends React.Component<
|
||||
key="Flash from URL"
|
||||
flow={{
|
||||
onClick: () => this.openURLSelector(),
|
||||
label: 'Flash from URL',
|
||||
label: i18next.t('source.fromURL'),
|
||||
icon: <LinkSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
@ -754,7 +755,7 @@ export class SourceSelector extends React.Component<
|
||||
key="Clone drive"
|
||||
flow={{
|
||||
onClick: () => this.openDriveSelector(),
|
||||
label: 'Clone drive',
|
||||
label: i18next.t('source.clone'),
|
||||
icon: <CopySvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
@ -775,7 +776,7 @@ export class SourceSelector extends React.Component<
|
||||
<span>{this.state.warning.title}</span>
|
||||
</span>
|
||||
}
|
||||
action="Continue"
|
||||
action={i18next.t('continue')}
|
||||
cancel={() => {
|
||||
this.setState({ warning: null });
|
||||
this.reselectSource();
|
||||
@ -793,17 +794,17 @@ export class SourceSelector extends React.Component<
|
||||
|
||||
{showImageDetails && (
|
||||
<SmallModal
|
||||
title="Image"
|
||||
title={i18next.t('source.image')}
|
||||
done={() => {
|
||||
this.setState({ showImageDetails: false });
|
||||
}}
|
||||
>
|
||||
<Txt.p>
|
||||
<Txt.span bold>Name: </Txt.span>
|
||||
<Txt.span bold>{i18next.t('source.name')}</Txt.span>
|
||||
<Txt.span>{imageName || imageBasename}</Txt.span>
|
||||
</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.p>
|
||||
</SmallModal>
|
||||
@ -842,8 +843,8 @@ export class SourceSelector extends React.Component<
|
||||
<DriveSelector
|
||||
write={false}
|
||||
multipleSelection={false}
|
||||
titleLabel="Select source"
|
||||
emptyListLabel="Plug a source drive"
|
||||
titleLabel={i18next.t('source.selectSource')}
|
||||
emptyListLabel={i18next.t('source.plugSource')}
|
||||
emptyListIcon={<SrcSvg width="40px" />}
|
||||
cancel={(originalList) => {
|
||||
if (originalList.length) {
|
||||
|
@ -32,6 +32,7 @@ import {
|
||||
StepNameButton,
|
||||
} from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import i18next from 'i18next';
|
||||
|
||||
interface TargetSelectorProps {
|
||||
targets: any[];
|
||||
@ -95,7 +96,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
<ChangeButton plain mb={14} onClick={props.reselectDrive}>
|
||||
Change
|
||||
{i18next.t('target.change')}
|
||||
</ChangeButton>
|
||||
)}
|
||||
{target.size != null && (
|
||||
@ -132,11 +133,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{targets.length} Targets
|
||||
{targets.length} {i18next.t('target.targets')}
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
<ChangeButton plain onClick={props.reselectDrive} mb={14}>
|
||||
Change
|
||||
{i18next.t('target.change')}
|
||||
</ChangeButton>
|
||||
)}
|
||||
{targetsTemplate}
|
||||
@ -151,7 +152,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
disabled={props.disabled}
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
Select target
|
||||
{i18next.t('target.selectTarget')}
|
||||
</StepButton>
|
||||
);
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ import TgtSvg from '../../../assets/tgt.svg';
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
import { warning } from '../../../../shared/messages';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import i18next from 'i18next';
|
||||
|
||||
export const getDriveListLabel = () => {
|
||||
return getSelectedDrives()
|
||||
@ -60,8 +61,8 @@ export const TargetSelectorModal = (
|
||||
) => (
|
||||
<DriveSelector
|
||||
multipleSelection={true}
|
||||
titleLabel="Select target"
|
||||
emptyListLabel="Plug a target drive"
|
||||
titleLabel={i18next.t('target.selectTarget')}
|
||||
emptyListLabel={i18next.t('target.plugTarget')}
|
||||
emptyListIcon={<TgtSvg width="40px" />}
|
||||
showWarnings={true}
|
||||
selectedList={getSelectedDrives()}
|
||||
|
318
lib/gui/app/i18n.ts
Normal file
318
lib/gui/app/i18n.ts
Normal file
@ -0,0 +1,318 @@
|
||||
import i18next from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
let lang = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
lang = lang.substr(0, 2);
|
||||
|
||||
i18next.use(initReactI18next).init({
|
||||
lng: lang,
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
resources: {
|
||||
zh: {
|
||||
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: '烧录失败',
|
||||
to: '到 ',
|
||||
andFail: '并烧录失败了 ',
|
||||
target_one: ' 个目标',
|
||||
target_other: ' 个目标',
|
||||
succeedTo: '被成功烧录',
|
||||
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)。',
|
||||
missingPartitionTable:
|
||||
'看起来这不是一个可启动的镜像。\n\n这个镜像似乎不包含分区表,因此您的设备可能无法识别或无法正确启动。',
|
||||
driveMissingPartitionTable:
|
||||
'看起来这不是可引导磁盘。\n这个磁盘似乎不包含分区表,\n因此您的设备可能无法识别或无法正确启动。',
|
||||
largeDriveSize:
|
||||
'这是个很大的磁盘!请检查并确认它不包含对您很重要的信息',
|
||||
systemDrive: '选择系统盘很危险,因为这将会删除你的系统',
|
||||
sourceDrive: '源镜像位于这个分区中',
|
||||
noSpace: '磁盘空间不足。 请插入另一个较大的磁盘并重试。',
|
||||
genericFlashError:
|
||||
'出了点问题。如果源镜像曾被压缩过,请检查它是否已损坏。',
|
||||
validation:
|
||||
'写入已成功完成,但 Etcher 在从磁盘读取镜像时检测到潜在的损坏问题。 \n\n请考虑将镜像写入其他磁盘。',
|
||||
openError: '打开 {{source}} 时出错。\n\n错误信息: {{error}}',
|
||||
flashError: '烧录 {{image}} 到 {{target}} 失败。',
|
||||
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: '速度:',
|
||||
failedTarget: '失败的烧录目标',
|
||||
failedRetry: '重试烧录失败目标',
|
||||
},
|
||||
settings: {
|
||||
errorReporting: '匿名地向 balena.io 报告运行错误和使用统计',
|
||||
autoUpdate: '自动更新',
|
||||
settings: '软件设置',
|
||||
systemInformation: '系统信息',
|
||||
},
|
||||
},
|
||||
},
|
||||
en: {
|
||||
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: 'Verifying...',
|
||||
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',
|
||||
to: 'to ',
|
||||
andFail: 'and failed to be flashed to ',
|
||||
target_one: ' target',
|
||||
target_other: ' targets',
|
||||
succeedTo: 'was successfully flashed',
|
||||
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).',
|
||||
missingPartitionTable:
|
||||
'It looks like this is not a bootable image.\n\nThe image does not appear to contain a partition table, and might not be recognized or bootable by your device.',
|
||||
driveMissingPartitionTable:
|
||||
'It looks like this is not a bootable drive.\nThe drive does not appear to contain a partition table,\nand 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.',
|
||||
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}} to {{target}}.',
|
||||
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: ',
|
||||
failedTarget: 'Failed targets',
|
||||
failedRetry: 'Retry failed targets',
|
||||
},
|
||||
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 i18next;
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import i18next from 'i18next';
|
||||
|
||||
export interface FlashState {
|
||||
active: number;
|
||||
@ -34,36 +35,45 @@ export function fromFlashState({
|
||||
position?: string;
|
||||
} {
|
||||
if (type === undefined) {
|
||||
return { status: 'Starting...' };
|
||||
return { status: i18next.t('progress.starting') };
|
||||
} else if (type === 'decompressing') {
|
||||
if (percentage == null) {
|
||||
return { status: 'Decompressing...' };
|
||||
return { status: i18next.t('progress.decompressing') };
|
||||
} else {
|
||||
return { position: `${percentage}%`, status: 'Decompressing...' };
|
||||
return {
|
||||
position: `${percentage}%`,
|
||||
status: i18next.t('progress.decompressing'),
|
||||
};
|
||||
}
|
||||
} else if (type === 'flashing') {
|
||||
if (percentage != null) {
|
||||
if (percentage < 100) {
|
||||
return { position: `${percentage}%`, status: 'Flashing...' };
|
||||
return {
|
||||
position: `${percentage}%`,
|
||||
status: i18next.t('progress.flashing'),
|
||||
};
|
||||
} else {
|
||||
return { status: 'Finishing...' };
|
||||
return { status: i18next.t('progress.finishing') };
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: 'Flashing...',
|
||||
status: i18next.t('progress.flashing'),
|
||||
position: `${position ? prettyBytes(position) : ''}`,
|
||||
};
|
||||
}
|
||||
} else if (type === 'verifying') {
|
||||
if (percentage == null) {
|
||||
return { status: 'Validating...' };
|
||||
return { status: i18next.t('progress.verifying') };
|
||||
} else if (percentage < 100) {
|
||||
return { position: `${percentage}%`, status: 'Validating...' };
|
||||
return {
|
||||
position: `${percentage}%`,
|
||||
status: i18next.t('progress.verifying'),
|
||||
};
|
||||
} else {
|
||||
return { status: 'Finishing...' };
|
||||
return { status: i18next.t('progress.finishing') };
|
||||
}
|
||||
}
|
||||
return { status: 'Failed' };
|
||||
return { status: i18next.t('progress.failing') };
|
||||
}
|
||||
|
||||
export function titleFromFlashState(
|
||||
|
@ -20,6 +20,7 @@ import * as _ from 'lodash';
|
||||
import * as errors from '../../../shared/errors';
|
||||
import * as settings from '../../../gui/app/models/settings';
|
||||
import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
|
||||
import i18next from 'i18next';
|
||||
|
||||
async function mountSourceDrive() {
|
||||
// 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'],
|
||||
filters: [
|
||||
{
|
||||
name: 'OS Images',
|
||||
name: i18next.t('source.osImages'),
|
||||
extensions: SUPPORTED_EXTENSIONS,
|
||||
},
|
||||
{
|
||||
name: 'All',
|
||||
name: i18next.t('source.allFiles'),
|
||||
extensions: ['*'],
|
||||
},
|
||||
],
|
||||
@ -79,8 +80,8 @@ export async function showWarning(options: {
|
||||
description: string;
|
||||
}): Promise<boolean> {
|
||||
_.defaults(options, {
|
||||
confirmationLabel: 'OK',
|
||||
rejectionLabel: 'Cancel',
|
||||
confirmationLabel: i18next.t('ok'),
|
||||
rejectionLabel: i18next.t('cancel'),
|
||||
});
|
||||
|
||||
const BUTTONS = [options.confirmationLabel, options.rejectionLabel];
|
||||
@ -98,7 +99,7 @@ export async function showWarning(options: {
|
||||
buttons: BUTTONS,
|
||||
defaultId: BUTTON_REJECTION_INDEX,
|
||||
cancelId: BUTTON_REJECTION_INDEX,
|
||||
title: 'Attention',
|
||||
title: i18next.t('attention'),
|
||||
message: options.title,
|
||||
detail: options.description,
|
||||
},
|
||||
|
@ -1,5 +1,11 @@
|
||||
// @ts-nocheck
|
||||
import { main } from './app';
|
||||
import './i18n';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
let lang = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
lang = lang.substr(0, 2);
|
||||
ipcRenderer.send('change-lng', lang);
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./app', () => {
|
||||
|
@ -21,18 +21,22 @@ import { platform } from 'os';
|
||||
import * as path from 'path';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import './app/i18n';
|
||||
|
||||
import { packageType, version } from '../../package.json';
|
||||
import * as EXIT_CODES from '../shared/exit-codes';
|
||||
import { delay, getConfig } from '../shared/utils';
|
||||
import * as settings from './app/models/settings';
|
||||
import { logException } from './app/modules/analytics';
|
||||
import { buildWindowMenu } from './menu';
|
||||
import i18n from 'i18next';
|
||||
|
||||
const customProtocol = 'etcher';
|
||||
const scheme = `${customProtocol}://`;
|
||||
const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
|
||||
const packageUpdatable = updatablePackageTypes.includes(packageType);
|
||||
let packageUpdated = false;
|
||||
let mainWindow: any = null;
|
||||
|
||||
async function checkForUpdates(interval: number) {
|
||||
// We use a while loop instead of a setInterval to preserve
|
||||
@ -130,7 +134,7 @@ async function createMainWindow() {
|
||||
if (fullscreen) {
|
||||
({ width, height } = electron.screen.getPrimaryDisplay().bounds);
|
||||
}
|
||||
const mainWindow = new electron.BrowserWindow({
|
||||
mainWindow = new electron.BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
frame: !fullscreen,
|
||||
@ -157,7 +161,6 @@ async function createMainWindow() {
|
||||
|
||||
electron.app.setAsDefaultProtocolClient(customProtocol);
|
||||
|
||||
buildWindowMenu(mainWindow);
|
||||
mainWindow.setFullScreen(true);
|
||||
|
||||
// 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(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. ');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,8 @@
|
||||
import * as electron from 'electron';
|
||||
import { displayName } from '../../package.json';
|
||||
|
||||
import i18next from 'i18next';
|
||||
|
||||
/**
|
||||
* @summary Builds a native application menu for a given window
|
||||
*/
|
||||
@ -42,12 +44,13 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
const menuTemplate: electron.MenuItemConstructorOptions[] = [
|
||||
{
|
||||
role: 'editMenu',
|
||||
label: i18next.t('menu.edit'),
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
label: i18next.t('menu.view'),
|
||||
submenu: [
|
||||
{
|
||||
label: 'Toggle Developer Tools',
|
||||
label: i18next.t('menu.devTool'),
|
||||
accelerator:
|
||||
process.platform === 'darwin' ? 'Command+Alt+I' : 'Control+Shift+I',
|
||||
click: toggleDevTools,
|
||||
@ -56,12 +59,14 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
},
|
||||
{
|
||||
role: 'windowMenu',
|
||||
label: i18next.t('menu.window'),
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
label: i18next.t('menu.help'),
|
||||
submenu: [
|
||||
{
|
||||
label: 'Etcher Pro',
|
||||
label: i18next.t('menu.pro'),
|
||||
click() {
|
||||
electron.shell.openExternal(
|
||||
'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() {
|
||||
electron.shell.openExternal('https://etcher.io?ref=etcher_menu');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Report an issue',
|
||||
label: i18next.t('menu.issue'),
|
||||
click() {
|
||||
electron.shell.openExternal(
|
||||
'https://github.com/balena-io/etcher/issues',
|
||||
@ -92,25 +97,29 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
submenu: [
|
||||
{
|
||||
role: 'about' as const,
|
||||
label: 'About Etcher',
|
||||
label: i18next.t('menu.about'),
|
||||
},
|
||||
{
|
||||
type: 'separator' as const,
|
||||
},
|
||||
{
|
||||
role: 'hide' as const,
|
||||
label: i18next.t('menu.hide'),
|
||||
},
|
||||
{
|
||||
role: 'hideOthers' as const,
|
||||
label: i18next.t('menu.hideOthers'),
|
||||
},
|
||||
{
|
||||
role: 'unhide' as const,
|
||||
label: i18next.t('menu.unhide'),
|
||||
},
|
||||
{
|
||||
type: 'separator' as const,
|
||||
},
|
||||
{
|
||||
role: 'quit' as const,
|
||||
label: i18next.t('menu.quit'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
21
lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js
Executable file
21
lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js
Executable 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)
|
||||
}
|
||||
|
@ -30,6 +30,9 @@ export async function sudo(
|
||||
command: string,
|
||||
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
|
||||
try {
|
||||
let lang = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
lang = lang.substr(0, 2);
|
||||
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
'sudo',
|
||||
['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`],
|
||||
@ -40,7 +43,7 @@ export async function sudo(
|
||||
SUDO_ASKPASS: join(
|
||||
getAppPath(),
|
||||
__dirname,
|
||||
'sudo-askpass.osascript.js',
|
||||
'sudo-askpass.osascript-' + lang + '.js',
|
||||
),
|
||||
},
|
||||
},
|
||||
|
@ -17,16 +17,16 @@
|
||||
import { Dictionary } from 'lodash';
|
||||
import { outdent } from 'outdent';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import '../gui/app/i18n';
|
||||
import i18next from 'i18next';
|
||||
|
||||
export const progress: Dictionary<(quantity: number) => string> = {
|
||||
successful: (quantity: number) => {
|
||||
const plural = quantity === 1 ? '' : 's';
|
||||
return `Successful target${plural}`;
|
||||
return i18next.t('message.flashSucceed', { count: quantity });
|
||||
},
|
||||
|
||||
failed: (quantity: number) => {
|
||||
const plural = quantity === 1 ? '' : 's';
|
||||
return `Failed target${plural}`;
|
||||
return i18next.t('message.flashFail', { count: quantity });
|
||||
},
|
||||
};
|
||||
|
||||
@ -38,129 +38,116 @@ export const info = {
|
||||
) => {
|
||||
const targets = [];
|
||||
if (failed + successful === 1) {
|
||||
targets.push(`to ${drive.description} (${drive.displayName})`);
|
||||
targets.push(
|
||||
i18next.t('message.to') + `${drive.description} (${drive.displayName})`,
|
||||
);
|
||||
} else {
|
||||
if (successful) {
|
||||
const plural = successful === 1 ? '' : 's';
|
||||
targets.push(`to ${successful} target${plural}`);
|
||||
targets.push(
|
||||
i18next.t('message.to') +
|
||||
successful +
|
||||
i18next.t('message.target', { count: successful }),
|
||||
);
|
||||
}
|
||||
if (failed) {
|
||||
const plural = failed === 1 ? '' : 's';
|
||||
targets.push(`and failed to be flashed to ${failed} target${plural}`);
|
||||
targets.push(
|
||||
i18next.t('message.andFail') +
|
||||
failed +
|
||||
i18next.t('message.target', { count: failed }),
|
||||
);
|
||||
}
|
||||
}
|
||||
return `${imageBasename} was successfully flashed ${targets.join(' ')}`;
|
||||
return (
|
||||
`${imageBasename} ` +
|
||||
i18next.t('message.succeedTo') +
|
||||
` ${targets.join(' ')}`
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const compatibility = {
|
||||
sizeNotRecommended: () => {
|
||||
return 'Not recommended';
|
||||
return i18next.t('message.sizeNotRecommended');
|
||||
},
|
||||
|
||||
tooSmall: () => {
|
||||
return 'Too small';
|
||||
return i18next.t('message.tooSmall');
|
||||
},
|
||||
|
||||
locked: () => {
|
||||
return 'Locked';
|
||||
return i18next.t('message.locked');
|
||||
},
|
||||
|
||||
system: () => {
|
||||
return 'System drive';
|
||||
return i18next.t('message.system');
|
||||
},
|
||||
|
||||
containsImage: () => {
|
||||
return 'Source drive';
|
||||
return i18next.t('message.containsImage');
|
||||
},
|
||||
|
||||
// The drive is large and therefore likely not a medium you want to write to.
|
||||
largeDrive: () => {
|
||||
return 'Large drive';
|
||||
return i18next.t('message.largeDrive');
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const warning = {
|
||||
tooSmall: (source: { size: number }, target: { size: number }) => {
|
||||
return outdent({ newline: ' ' })`
|
||||
The selected source is ${prettyBytes(source.size - target.size)}
|
||||
larger than this drive.
|
||||
${i18next.t('message.sourceLarger', {
|
||||
byte: prettyBytes(source.size - target.size),
|
||||
})}
|
||||
`;
|
||||
},
|
||||
|
||||
exitWhileFlashing: () => {
|
||||
return [
|
||||
'You are currently flashing a drive.',
|
||||
'Closing Etcher may leave your drive in an unusable state.',
|
||||
].join(' ');
|
||||
return i18next.t('message.exitWhileFlashing');
|
||||
},
|
||||
|
||||
looksLikeWindowsImage: () => {
|
||||
return [
|
||||
'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(' ');
|
||||
return i18next.t('message.looksLikeWindowsImage');
|
||||
},
|
||||
|
||||
missingPartitionTable: () => {
|
||||
return [
|
||||
'It looks like this is not a bootable image.\n\n',
|
||||
'The image does not appear to contain a partition table,',
|
||||
'and might not be recognized or bootable by your device.',
|
||||
].join(' ');
|
||||
return i18next.t('message.missingPartitionTable');
|
||||
},
|
||||
|
||||
driveMissingPartitionTable: () => {
|
||||
return outdent({ newline: ' ' })`
|
||||
It looks like this is not a bootable drive.
|
||||
The drive does not appear to contain a partition table,
|
||||
and might not be recognized or bootable by your device.
|
||||
`;
|
||||
return i18next.t('message.driveMissingPartitionTable');
|
||||
},
|
||||
|
||||
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: () => {
|
||||
return 'Selecting your system drive is dangerous and will erase your drive!';
|
||||
return i18next.t('message.systemDrive');
|
||||
},
|
||||
|
||||
sourceDrive: () => {
|
||||
return 'Contains the image you chose to flash';
|
||||
return i18next.t('message.sourceDrive');
|
||||
},
|
||||
};
|
||||
|
||||
export const error = {
|
||||
notEnoughSpaceInDrive: () => {
|
||||
return [
|
||||
'Not enough space on the drive.',
|
||||
'Please insert larger one and try again.',
|
||||
].join(' ');
|
||||
return i18next.t('message.noSpace');
|
||||
},
|
||||
|
||||
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') + `\n${err.message}`;
|
||||
},
|
||||
|
||||
validation: () => {
|
||||
return [
|
||||
'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(' ');
|
||||
return i18next.t('message.validation');
|
||||
},
|
||||
|
||||
openSource: (sourceName: string, errorMessage: string) => {
|
||||
return outdent`
|
||||
Something went wrong while opening ${sourceName}
|
||||
|
||||
Error: ${errorMessage}
|
||||
`;
|
||||
return i18next.t('message.openError', {
|
||||
source: sourceName,
|
||||
error: errorMessage,
|
||||
});
|
||||
},
|
||||
|
||||
flashFailure: (
|
||||
@ -170,34 +157,24 @@ export const error = {
|
||||
const target =
|
||||
drives.length === 1
|
||||
? `${drives[0].description} (${drives[0].displayName})`
|
||||
: `${drives.length} targets`;
|
||||
return `Something went wrong while writing ${imageBasename} to ${target}.`;
|
||||
: `${drives.length}` +
|
||||
i18next.t('message.target', { count: drives.length });
|
||||
return i18next.t('message.flashError', { image: imageBasename, target });
|
||||
},
|
||||
|
||||
driveUnplugged: () => {
|
||||
return [
|
||||
'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(' ');
|
||||
return i18next.t('message.unplug');
|
||||
},
|
||||
|
||||
inputOutput: () => {
|
||||
return [
|
||||
'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(' ');
|
||||
return i18next.t('message.cannotWrite');
|
||||
},
|
||||
|
||||
childWriterDied: () => {
|
||||
return [
|
||||
'The writer process ended unexpectedly.',
|
||||
'Please try again, and contact the Etcher team if the problem persists.',
|
||||
].join(' ');
|
||||
return i18next.t('message.childWriterDied');
|
||||
},
|
||||
|
||||
unsupportedProtocol: () => {
|
||||
return 'Only http:// and https:// URLs are supported.';
|
||||
return i18next.t('message.badProtocol');
|
||||
},
|
||||
};
|
||||
|
54
package-lock.json
generated
54
package-lock.json
generated
@ -2353,7 +2353,6 @@
|
||||
"version": "7.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
|
||||
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
@ -9888,6 +9887,14 @@
|
||||
"integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"requires": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"html-void-elements": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz",
|
||||
@ -10108,6 +10115,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"i18next": {
|
||||
"version": "21.8.14",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/i18next/-/i18next-21.8.14.tgz",
|
||||
"integrity": "sha512-4Yi+DtexvMm/Yw3Q9fllzY12SgLk+Mcmar+rCAccsOPul/2UmnBzoHbTGn/L48IPkFcmrNaH7xTLboBWIbH6pw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.17.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/runtime/-/runtime-7.18.6.tgz",
|
||||
"integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"iconv-corefoundation": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
|
||||
@ -13967,6 +13992,16 @@
|
||||
"buffer": "^5.2.1",
|
||||
"through": "^2.3.8"
|
||||
}
|
||||
},
|
||||
"unbzip2-stream": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
|
||||
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"buffer": "^5.2.1",
|
||||
"through": "^2.3.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -14193,6 +14228,15 @@
|
||||
"react-side-effect": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"react-i18next": {
|
||||
"version": "11.18.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/react-i18next/-/react-i18next-11.18.1.tgz",
|
||||
"integrity": "sha512-S8cl4mvIOSA7OQCE5jNy2yhv705Vwi+7PinpqKIYcBmX/trJtHKqrf6CL67WJSA8crr2JU+oxE9jn9DQIrQezg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.14.5",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@ -14386,8 +14430,7 @@
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.9",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
||||
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
|
||||
},
|
||||
"regenerator-transform": {
|
||||
"version": "0.14.5",
|
||||
@ -17098,6 +17141,11 @@
|
||||
"unist-util-stringify-position": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="
|
||||
},
|
||||
"watchpack": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
|
@ -127,3 +127,4 @@
|
||||
"publishedAt": "2022-12-13T02:27:42.254Z"
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user