Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ was recorded with StepForge.
- **Publish to Confluence** (beta) on Confluence Cloud or your organization's
Data Center site, including sites that need a smart card (CAC).
- **Publish to Wiki.js** (beta), with the screenshots uploaded to the wiki.
- **Six languages.** English, plus French, Spanish, Russian, Simplified
Chinese and Japanese (beta). StepForge follows your system language, or
pick one in **Settings → General → Language**. Exported guides stay in
English.

**Optional, and off until you turn them on**

Expand Down
61 changes: 44 additions & 17 deletions app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const security = require('./security');
const { linuxPasswordStore } = require('./platform/linux/password-store');
const { syncedFolders, syncServiceFor } = require('./synced-folders');
const windowsStore = require('./windows-store');
const i18n = require('../core/i18n');
const LOCALES = require('../core/locales');
const PACKAGE_JSON = require(path.join(__dirname, '..', 'package.json'));
// Linux packages launch the bundled Electron against /opt/stepforge, so
// app.isPackaged is false there too. Only a source checkout is a dev build.
Expand Down Expand Up @@ -192,6 +194,21 @@ function applyTheme() {
nativeTheme.themeSource = settings.get('appearance') || 'system';
}

/** The interface language from Settings → General → Language ('system' follows the OS). */
function uiLanguage() {
return i18n.resolveLanguage(settings.get('language'), app.getPreferredSystemLanguages());
}

// Translates the main process's own text: file dialog titles.
let mainText = i18n.createTranslator('en');
const t = (text, params) => mainText.t(text, params);

/** Loads index.html in the interface language; the renderer reads it from ?lang=. */
function loadMainPage() {
mainText = i18n.createTranslator(uiLanguage(), LOCALES);
return mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'), { query: { lang: mainText.language } });
}

/** Code block colors for an export, from Settings → Exports. */
function exportCodeStyle() {
return resolveCodeStyle(settings.get('exports.codeTheme'));
Expand Down Expand Up @@ -292,7 +309,7 @@ function createWindow() {
event.preventDefault();
handleZoomShortcut(kind);
});
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
loadMainPage();
mainWindow.once('ready-to-show', () => {
mainWindow.show();
// Dev-only verification hook: optionally navigate, then write a
Expand Down Expand Up @@ -969,7 +986,7 @@ function setupIpc() {
}, { validate: (a) => c.id(a.guideId) && c.optionalNumber(a.position, 0, 100000) });
h('step:importImage', async ({ guideId }) => {
const res = await dialog.showOpenDialog(mainWindow, {
title: 'Import images as steps',
title: t('Import images as steps'),
filters: [{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp'] }],
properties: ['openFile', 'multiSelections'],
});
Expand Down Expand Up @@ -1003,7 +1020,7 @@ function setupIpc() {
h('step:chooseImage', async ({ guideId, stepId }) => {
store.getStep(guideId, stepId); // throws for a step that does not exist, before any dialog
const res = await dialog.showOpenDialog(mainWindow, {
title: 'Choose a screenshot for this step',
title: t('Choose a screenshot for this step'),
filters: [{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'] }],
properties: ['openFile'],
});
Expand Down Expand Up @@ -1349,7 +1366,7 @@ function setupIpc() {
h('storage:choose', async () => {
if (libraryLocation.status().locked) throw new Error('STEPFORGE_DATA_DIR controls this session. Remove that override before changing storage.');
const result = await dialog.showOpenDialog(mainWindow, {
title: 'Choose guide storage folder',
title: t('Choose guide storage folder'),
properties: ['openDirectory', 'createDirectory'],
});
if (result.canceled || !result.filePaths[0]) return { cancelled: true, ...storageStatus(libraryLocation.status()) };
Expand All @@ -1359,6 +1376,7 @@ function setupIpc() {
h('storage:cancelMove', () => storageStatus(libraryLocation.cancel()));
h('settings:set', ({ keyPath, value }) => {
if (keyPath === 'cloud' || keyPath.startsWith('cloud.')) throw new Error('Use the cloud sync controls in Settings → Accounts.');
if (keyPath === 'language') throw new Error('Use Settings → General → Language.');
const trashWasKept = !(settings.get('library.trashDays') > 0);
settings.set(keyPath, value);
// Turning emptying on starts the clock now for guides already in the trash.
Expand Down Expand Up @@ -1492,7 +1510,7 @@ function setupIpc() {
h('archive:export', async ({ guideId }) => {
const guide = store.getGuide(guideId);
const res = await dialog.showSaveDialog(mainWindow, {
title: 'Share guide as file',
title: t('Share guide as file'),
defaultPath: `${guide.title.replace(/[/\\:]+/g, '-')}.sfgz`,
filters: [{ name: 'StepForge guide archive', extensions: ['sfgz'] }],
});
Expand All @@ -1502,7 +1520,7 @@ function setupIpc() {
}, { validate: (a) => c.id(a.guideId) });
h('archive:open', async ({ mode }) => {
const res = await dialog.showOpenDialog(mainWindow, {
title: 'Open guide archive',
title: t('Open guide archive'),
filters: [{ name: 'StepForge guide archive', extensions: ['sfgz'] }],
properties: ['openFile'],
});
Expand Down Expand Up @@ -1637,7 +1655,7 @@ function setupIpc() {
if (!dir) dir = settings.get(`exports.lastOutputDirs.${format}`);
if (!dir) {
const res = await dialog.showOpenDialog(mainWindow, {
title: 'Choose output folder', properties: ['openDirectory', 'createDirectory'],
title: t('Choose output folder'), properties: ['openDirectory', 'createDirectory'],
});
if (res.canceled) return { ok: false };
dir = res.filePaths[0];
Expand All @@ -1664,14 +1682,14 @@ function setupIpc() {
});
h('export:chooseImage', async () => {
const res = await dialog.showOpenDialog(mainWindow, {
title: 'Choose an image', properties: ['openFile'],
title: t('Choose an image'), properties: ['openFile'],
filters: [{ name: 'PNG image', extensions: ['png'] }],
});
return res.canceled ? null : res.filePaths[0];
});
h('export:chooseDir', async ({ format }) => {
const res = await dialog.showOpenDialog(mainWindow, {
title: 'Choose output folder', properties: ['openDirectory', 'createDirectory'],
title: t('Choose output folder'), properties: ['openDirectory', 'createDirectory'],
});
if (res.canceled) return null;
chosenOutputDirs.add(res.filePaths[0]);
Expand Down Expand Up @@ -1711,9 +1729,9 @@ function setupIpc() {
};
const assetId = (v) => typeof v === 'string' && /^[a-f0-9]{32}$/.test(v);
const ASSET_FILTERS = {
image: { title: 'Choose an image', filters: [{ name: 'Images (PNG, JPG, SVG)', extensions: ['png', 'jpg', 'jpeg', 'svg'] }] },
font: { title: 'Choose a font file', filters: [{ name: 'Fonts (WOFF2, WOFF, TTF, OTF)', extensions: ['woff2', 'woff', 'ttf', 'otf'] }] },
css: { title: 'Choose a CSS file', filters: [{ name: 'CSS', extensions: ['css'] }] },
image: { title: () => t('Choose an image'), filters: [{ name: 'Images (PNG, JPG, SVG)', extensions: ['png', 'jpg', 'jpeg', 'svg'] }] },
font: { title: () => t('Choose a font file'), filters: [{ name: 'Fonts (WOFF2, WOFF, TTF, OTF)', extensions: ['woff2', 'woff', 'ttf', 'otf'] }] },
css: { title: () => t('Choose a CSS file'), filters: [{ name: 'CSS', extensions: ['css'] }] },
};
h('branding:list', () => branding.list());
h('branding:get', ({ id }) => brandProfile(id), { validate: (a) => validBrandId(a.id) });
Expand All @@ -1726,7 +1744,7 @@ function setupIpc() {
}, { validate: (a) => validBrandId(a.id) });
h('branding:duplicate', ({ id }) => brandProfile(branding.duplicate(id).id), { validate: (a) => validBrandId(a.id) });
h('branding:chooseAsset', async ({ kind }) => {
const res = await dialog.showOpenDialog(mainWindow, { ...ASSET_FILTERS[kind], properties: ['openFile'] });
const res = await dialog.showOpenDialog(mainWindow, { ...ASSET_FILTERS[kind], title: ASSET_FILTERS[kind].title(), properties: ['openFile'] });
if (res.canceled) return null;
const file = res.filePaths[0];
// Too large is refused as soon as it's picked, before the file is read.
Expand Down Expand Up @@ -1835,6 +1853,15 @@ function setupIpc() {
});
return lastUpdateCheck;
});
// Settings → General → Language. Saves the choice and, when the shown
// language changes, reloads the window in it. The renderer saves open
// edits first.
h('app:setLanguage', ({ language }) => {
settings.set('language', language);
if (uiLanguage() === mainText.language) return { ok: true, reloaded: false };
void loadMainPage();
return { ok: true, reloaded: true };
}, { validate: (a) => c.oneOf(a.language, ['system', ...i18n.LANGUAGES.map((l) => l.code)]) });
// Settings → About "Open Microsoft Store" (Store installs): StepForge's Store
// page, where Update installs the new version. The URL is built here.
h('app:openStore', async () => {
Expand Down Expand Up @@ -1934,10 +1961,10 @@ if (!gotLock) {
certificates: { ...(settings.get('confluence')?.certificates || {}), [host]: fingerprint } }),
ask: async (host, certificates) => {
const { response } = await dialog.showMessageBox(mainWindow, {
type: 'question', title: 'Choose a certificate',
message: `${host} asks for a certificate to sign you in.`,
detail: 'Choose the one from your smart card that you use to sign in to websites. StepForge remembers your choice for this site.',
buttons: [...certificates.map((cert) => `${cert.subjectName} (issued by ${cert.issuerName})`.slice(0, 120)), 'Cancel'],
type: 'question', title: t('Choose a certificate'),
message: t('{host} asks for a certificate to sign you in.', { host }),
detail: t('Choose the one from your smart card that you use to sign in to websites. StepForge remembers your choice for this site.'),
buttons: [...certificates.map((cert) => t('{subject} (issued by {issuer})', { subject: cert.subjectName, issuer: cert.issuerName }).slice(0, 120)), t('Cancel')],
cancelId: certificates.length, noLink: true,
});
return response >= certificates.length ? -1 : response;
Expand Down
1 change: 1 addition & 0 deletions app/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ const api = {
checkForUpdates: invoke('app:checkForUpdates'),
updateNow: invoke('app:updateNow'),
openStore: invoke('app:openStore'),
setLanguage: invoke('app:setLanguage'),
platformCapabilities: invoke('platform:capabilities'),
},
};
Expand Down
Loading
Loading