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
|
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
|
||||||
|
@ -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 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) {
|
||||||
|
@ -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 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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -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 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>
|
||||||
|
@ -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 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 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,10 @@ 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'}!
|
{i18next.t('flash.flash')}{' '}
|
||||||
|
{allFailed ? i18next.t('failed') : i18next.t('completed')}!
|
||||||
</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 +190,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 +201,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')} {effectiveSpeed} MB/s
|
||||||
</Txt>
|
</Txt>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -214,11 +213,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);
|
||||||
|
@ -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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 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>
|
||||||
)}
|
)}
|
||||||
|
@ -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 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) {
|
||||||
|
@ -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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 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()}
|
||||||
|
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 * as prettyBytes from 'pretty-bytes';
|
||||||
|
import 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(
|
||||||
|
@ -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 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,
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { main } from './app';
|
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) {
|
if (module.hot) {
|
||||||
module.hot.accept('./app', () => {
|
module.hot.accept('./app', () => {
|
||||||
|
@ -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 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. ');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 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'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
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,
|
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',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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 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,116 @@ 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.to') + `${drive.description} (${drive.displayName})`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (successful) {
|
if (successful) {
|
||||||
const plural = successful === 1 ? '' : 's';
|
targets.push(
|
||||||
targets.push(`to ${successful} target${plural}`);
|
i18next.t('message.to') +
|
||||||
|
successful +
|
||||||
|
i18next.t('message.target', { count: 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.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 = {
|
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',
|
|
||||||
'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.driveMissingPartitionTable');
|
||||||
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.
|
|
||||||
`;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
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') + `\n${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: (
|
||||||
@ -170,34 +157,24 @@ export const error = {
|
|||||||
const target =
|
const target =
|
||||||
drives.length === 1
|
drives.length === 1
|
||||||
? `${drives[0].description} (${drives[0].displayName})`
|
? `${drives[0].description} (${drives[0].displayName})`
|
||||||
: `${drives.length} targets`;
|
: `${drives.length}` +
|
||||||
return `Something went wrong while writing ${imageBasename} to ${target}.`;
|
i18next.t('message.target', { count: drives.length });
|
||||||
|
return i18next.t('message.flashError', { image: imageBasename, 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');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
54
package-lock.json
generated
54
package-lock.json
generated
@ -2353,7 +2353,6 @@
|
|||||||
"version": "7.16.3",
|
"version": "7.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
|
||||||
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
|
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"regenerator-runtime": "^0.13.4"
|
"regenerator-runtime": "^0.13.4"
|
||||||
}
|
}
|
||||||
@ -9888,6 +9887,14 @@
|
|||||||
"integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==",
|
"integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==",
|
||||||
"dev": true
|
"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": {
|
"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 +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": {
|
"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",
|
||||||
@ -13967,6 +13992,16 @@
|
|||||||
"buffer": "^5.2.1",
|
"buffer": "^5.2.1",
|
||||||
"through": "^2.3.8"
|
"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-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": {
|
"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",
|
||||||
@ -14386,8 +14430,7 @@
|
|||||||
"regenerator-runtime": {
|
"regenerator-runtime": {
|
||||||
"version": "0.13.9",
|
"version": "0.13.9",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
||||||
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
|
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"regenerator-transform": {
|
"regenerator-transform": {
|
||||||
"version": "0.14.5",
|
"version": "0.14.5",
|
||||||
@ -17098,6 +17141,11 @@
|
|||||||
"unist-util-stringify-position": "^2.0.0"
|
"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": {
|
"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",
|
||||||
|
@ -127,3 +127,4 @@
|
|||||||
"publishedAt": "2022-12-13T02:27:42.254Z"
|
"publishedAt": "2022-12-13T02:27:42.254Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user