fix: i18n not loaded

This commit is contained in:
tiddlygit-test 2021-03-11 00:21:26 +08:00
parent 8305a88965
commit 301b4b0205
8 changed files with 363 additions and 420 deletions

View file

@ -1,345 +0,0 @@
/* eslint-disable unicorn/prevent-abbreviations */
import { BackendModule } from 'i18next';
import { cloneDeep, merge } from 'lodash';
// CONFIGS
const defaultOptions = {
debug: false,
loadPath: '/locales/{{lng}}/{{ns}}.json',
addPath: '/locales/{{lng}}/{{ns}}.missing.json',
};
const readFileRequest = 'ReadFile-Request';
const writeFileRequest = 'WriteFile-Request';
const readFileResponse = 'ReadFile-Response';
const writeFileResponse = 'WriteFile-Response';
const changeLanguageRequest = 'ChangeLanguage-Request';
/**
* Fast UUID generator, RFC4122 version 4 compliant.
* @author Jeff Ward (jcward.com).
* @license MIT license
* @link http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136
**/
const UUID = (function () {
const self = {};
const lut: any = [];
for (let index = 0; index < 256; index++) {
lut[index] = (index < 16 ? '0' : '') + index.toString(16);
}
(self as any).generate = function () {
const d0 = Math.trunc(Math.random() * 0xffffffff);
const d1 = Math.trunc(Math.random() * 0xffffffff);
const d2 = Math.trunc(Math.random() * 0xffffffff);
const d3 = Math.trunc(Math.random() * 0xffffffff);
return `${lut[d0 & 0xff] + lut[(d0 >> 8) & 0xff] + lut[(d0 >> 16) & 0xff] + lut[(d0 >> 24) & 0xff]}-${lut[d1 & 0xff]}${lut[(d1 >> 8) & 0xff]}-${
lut[((d1 >> 16) & 0x0f) | 0x40]
}${lut[(d1 >> 24) & 0xff]}-${lut[(d2 & 0x3f) | 0x80]}${lut[(d2 >> 8) & 0xff]}-${lut[(d2 >> 16) & 0xff]}${lut[(d2 >> 24) & 0xff]}${lut[d3 & 0xff]}${
lut[(d3 >> 8) & 0xff]
}${lut[(d3 >> 16) & 0xff]}${lut[(d3 >> 24) & 0xff]}`;
};
return self;
})();
// Merges objects together
const mergeNested = function (object: any, path: any, split: any, value: any) {
const tokens = path.split(split);
let temporary = {};
let temporary2;
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
temporary[`${tokens[tokens.length - 1]}`] = value;
for (let index = tokens.length - 2; index >= 0; index--) {
temporary2 = {};
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
temporary2[`${tokens[index]}`] = temporary;
temporary = temporary2;
}
return merge(object, temporary);
};
// https://stackoverflow.com/a/34890276/1837080
const groupByArray = function (xs: any, key: any) {
return xs.reduce(function (rv: any, x: any) {
const v = key instanceof Function ? key(x) : x[key];
const element = rv.find((r: any) => r && r.key === v);
if (element) {
element.values.push(x);
} else {
rv.push({
key: v,
values: [x],
});
}
return rv;
}, []);
};
// Template is found at: https://www.i18next.com/misc/creating-own-plugins#backend;
// also took code from: https://github.com/i18next/i18next-node-fs-backend
export class Backend implements BackendModule {
static type = 'backend';
type = 'backend' as const;
backendOptions: any;
i18nextOptions: any;
mainLog: any;
readCallbacks: any;
rendererLog: any;
services: any;
useOverflow: any;
writeCallbacks: any;
writeQueue: any;
writeQueueOverflow: any;
writeTimeout: any;
constructor(services: any, backendOptions = {}, i18nextOptions = {}) {
this.init(services, backendOptions, i18nextOptions);
this.readCallbacks = {}; // Callbacks after reading a translation
this.writeCallbacks = {}; // Callbacks after writing a missing translation
this.writeTimeout; // A timer that will initate writing missing translations to files
this.writeQueue = []; // An array to hold missing translations before the writeTimeout occurs
this.writeQueueOverflow = []; // An array to hold missing translations while the writeTimeout's items are being written to file
this.useOverflow = false; // If true, we should insert missing translations into the writeQueueOverflow
}
init(services: any, backendOptions: any, i18nextOptions: any) {
if (typeof window !== 'undefined' && typeof (window as any).i18n.i18nextElectronBackend === 'undefined') {
throw new TypeError("'window.i18n.i18nextElectronBackend' is not defined! Be sure you are setting up your BrowserWindow's preload script properly!");
}
this.services = services;
this.backendOptions = {
...defaultOptions,
...backendOptions,
i18nextElectronBackend: typeof window !== 'undefined' ? (window as any).i18n.i18nextElectronBackend : undefined,
};
this.i18nextOptions = i18nextOptions;
// log-related
const logPrepend = '[i18next-electron-fs-backend:';
this.mainLog = `${logPrepend}main]=>`;
this.rendererLog = `${logPrepend}renderer]=>`;
this.setupIpcBindings();
}
// Sets up Ipc bindings so that we can keep any node-specific
// modules; (ie. 'fs') out of the Electron renderer process
setupIpcBindings() {
const { i18nextElectronBackend } = this.backendOptions;
i18nextElectronBackend.onReceive(readFileResponse, (arguments_: any) => {
// args:
// {
// key
// error
// data
// }
// Don't know why we need this line;
// upon initialization, the i18next library
// ends up in this .on([channel], args) method twice
if (typeof this.readCallbacks[arguments_.key] === 'undefined') {
return;
}
let callback;
if (arguments_.error) {
// Failed to read translation file;
// we pass back a fake "success" response
// so that we create a translation file
callback = this.readCallbacks[arguments_.key].callback;
delete this.readCallbacks[arguments_.key];
if (callback !== null && typeof callback === 'function') {
callback(null, {});
}
} else {
let result;
arguments_.data = arguments_.data.replace(/^\uFEFF/, '');
try {
result = JSON.parse(arguments_.data);
} catch (parseError) {
parseError.message = `Error parsing '${arguments_.filename}'. Message: '${parseError}'.`;
callback = this.readCallbacks[arguments_.key].callback;
delete this.readCallbacks[arguments_.key];
if (callback !== null && typeof callback === 'function') {
callback(parseError);
}
return;
}
callback = this.readCallbacks[arguments_.key].callback;
delete this.readCallbacks[arguments_.key];
if (callback !== null && typeof callback === 'function') {
callback(null, result);
}
}
});
i18nextElectronBackend.onReceive(writeFileResponse, (arguments_: any) => {
// args:
// {
// keys
// error
// }
const { keys } = arguments_;
for (const key of keys) {
let callback;
// Write methods don't have any callbacks from what I've seen,
// so this is called more than I thought; but necessary!
if (typeof this.writeCallbacks[key] === 'undefined') {
return;
}
if (arguments_.error) {
callback = this.writeCallbacks[key].callback;
delete this.writeCallbacks[key];
callback(arguments_.error);
} else {
callback = this.writeCallbacks[key].callback;
delete this.writeCallbacks[key];
callback(null, true);
}
}
});
}
// Writes a given translation to file
write(writeQueue: any) {
const { debug, i18nextElectronBackend } = this.backendOptions;
// Group by filename so we can make one request
// for all changes within a given file
const toWork = groupByArray(writeQueue, 'filename');
for (const element of toWork) {
const anonymous = function (error: any, data: any) {
if (error) {
console.error(
`${
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
this.rendererLog
} encountered error when trying to read file '{filename}' before writing missing translation ('{key}'/'{fallbackValue}') to file. Please resolve this error so missing translation values can be written to file. Error: '${error}'.`,
);
return;
}
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
const keySeparator = !!this.i18nextOptions.keySeparator; // Do we have a key separator or not?
const writeKeys = [];
for (let index = 0; index < element.values.length; index++) {
// If we have no key separator set, simply update the translation value
if (!keySeparator) {
data[element.values[index].key] = element.values[index].fallbackValue;
} else {
// Created the nested object structure based on the key separator, and merge that
// into the existing translation data
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
data = mergeNested(data, element.values[index].key, this.i18nextOptions.keySeparator, element.values[index].fallbackValue);
}
const writeKey = `${(UUID as any).generate()}`;
if (element.values[index].callback) {
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
this.writeCallbacks[writeKey] = {
callback: element.values[index].callback,
};
writeKeys.push(writeKey);
}
}
// Send out the message to the ipcMain process
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
debug ? console.log(`${this.rendererLog} requesting the missing key '${key}' be written to file '${filename}'.`) : null;
i18nextElectronBackend.send(writeFileRequest, {
keys: writeKeys,
filename: element.key,
data,
});
}.bind(this);
this.requestFileRead(element.key, anonymous);
}
}
// Reads a given translation file
requestFileRead(filename: any, callback: any) {
const { i18nextElectronBackend } = this.backendOptions;
// Save the callback for this request so we
// can execute once the ipcRender process returns
// with a value from the ipcMain process
const key = `${(UUID as any).generate()}`;
this.readCallbacks[key] = {
callback,
};
// Send out the message to the ipcMain process
i18nextElectronBackend.send(readFileRequest, {
key,
filename,
});
}
// Reads a given translation file
read(language: any, namespace: any, callback: any) {
const { loadPath } = this.backendOptions;
const filename = this.services.interpolator.interpolate(loadPath, {
lng: language,
ns: namespace,
});
this.requestFileRead(filename, (error: any, data: any) => {
if (error) {
return callback(error, false);
} // no retry
callback(null, data);
});
}
// Not implementing at this time
readMulti(languages: any, namespaces: any, callback: any) {
throw 'Not implemented exception.';
}
// Writes a missing translation to file
create(languages: any, namespace: any, key: any, fallbackValue: any, callback: any) {
const { addPath } = this.backendOptions;
let filename;
languages = typeof languages === 'string' ? [languages] : languages;
// Create the missing translation for all languages
for (const language of languages) {
filename = this.services.interpolator.interpolate(addPath, {
lng: language,
ns: namespace,
});
// If we are currently writing missing translations from writeQueue,
// temporarily store the requests in writeQueueOverflow until we are
// done writing to file
if (this.useOverflow) {
this.writeQueueOverflow.push({
filename,
key,
fallbackValue,
callback,
});
} else {
this.writeQueue.push({
filename,
key,
fallbackValue,
callback,
});
}
}
// Fire up the timeout to process items to write
if (this.writeQueue.length > 0 && !this.useOverflow) {
// Clear out any existing timeout if we are still getting translations to write
if (typeof this.writeTimeout !== 'undefined') {
clearInterval(this.writeTimeout);
}
this.writeTimeout = setInterval(
function () {
// Write writeQueue entries, then after,
// fill in any from the writeQueueOverflow
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
if (this.writeQueue.length > 0) {
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
this.write(cloneDeep(this.writeQueue));
}
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
this.writeQueue = cloneDeep(this.writeQueueOverflow);
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
this.writeQueueOverflow = [];
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
if (this.writeQueue.length === 0) {
// Clear timer
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
clearInterval(this.writeTimeout);
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
delete this.writeTimeout;
// @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message
this.useOverflow = false;
}
}.bind(this),
1000,
);
this.useOverflow = true;
}
}
}

View file

@ -1,6 +1,6 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { Backend as ElectronFsBackend } from './helpers/i18next-electron-fs-backend';
import { Backend as ElectronFsBackend } from './services/libs/i18n/i18next-electron-fs-backend';
export async function initI18N(): Promise<void> {
const isDevelopment = (await window.service.context.get('isDevelopment')) as boolean;

View file

@ -8,14 +8,14 @@ import unhandled from 'electron-unhandled';
import { openNewGitHubIssue, debugInfo } from 'electron-util';
import i18n from 'i18next';
import { clearMainBindings } from '@services/libs/i18n/i18next-electron-fs-backend';
import { clearMainBindings } from '@services/libs/i18n/i18nMainBindings';
import { buildLanguageMenu } from '@services/libs/i18n/buildLanguageMenu';
import { MainChannel } from '@/constants/channels';
import { container } from '@services/container';
import { logger } from '@services/libs/log';
import extractHostname from '@services/libs/extract-hostname';
import MAILTO_URLS from '@services/constants/mailto-urls';
import { initI18NAfterServiceReady } from '@services/libs/i18n';
import { initRendererI18NHandler } from '@services/libs/i18n';
import serviceIdentifier from '@services/serviceIdentifier';
import { WindowNames } from '@services/windows/WindowProperties';
@ -56,6 +56,7 @@ if (!gotTheLock) {
mainWindow.focus();
}
});
void initRendererI18NHandler();
// make sure "Settings" file exists
// if not, ignore this chunk of code
// as using electron-settings before app.on('ready') and "Settings" is created
@ -107,9 +108,8 @@ if (!gotTheLock) {
});
}
await windowService.open(WindowNames.main);
// set language async
// set language async on main process
void i18n.changeLanguage(preferenceService.get('language'));
await workspaceViewService.initializeAllWorkspaceView();
ipcMain.emit('request-update-pause-notifications-info');
@ -141,7 +141,6 @@ if (!gotTheLock) {
}
// trigger whenTrulyReady
ipcMain.emit(MainChannel.commonInitFinished);
await initI18NAfterServiceReady();
// build menu at last, this is not noticeable to user, so do it last
buildLanguageMenu();
menuService.buildMenu();

View file

@ -1,16 +0,0 @@
import { ipcMain } from 'electron';
import { container } from '@services/container';
import type { IWindowService } from '@services/windows/interface';
import { WindowNames } from '@services/windows/WindowProperties';
import serviceIdentifier from '@services/serviceIdentifier';
export default async function bindI18nListener(): Promise<void> {
const windows = container.get<IWindowService>(serviceIdentifier.Window);
const mainWindow = windows.get(WindowNames.main);
if (mainWindow === undefined) {
throw new Error('Window is undefined in bindI18nListener()');
}
const { mainBindings } = await import('./i18next-electron-fs-backend');
mainBindings(ipcMain, mainWindow);
}

View file

@ -0,0 +1,57 @@
import { ipcMain } from 'electron';
import fs from 'fs-extra';
import path from 'path';
import { IpcMain, IpcMainInvokeEvent } from 'electron';
import type { IWindowService } from '@services/windows/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import { container } from '@services/container';
import { LOCALIZATION_FOLDER } from '@services/constants/paths';
import { I18NChannels } from '@/constants/channels';
import { IReadFileRequest, IWriteFileRequest } from './types';
/**
* This is the code that will go into the main.js file
* in order to set up the ipc main bindings
*/
export function mainBindings(): void {
ipcMain.handle(I18NChannels.readFileRequest, (_event: IpcMainInvokeEvent, readFileArgs: IReadFileRequest) => {
const localeFilePath = path.join(LOCALIZATION_FOLDER, readFileArgs.filename);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
fs.readFile(localeFilePath, 'utf8', (error, data) => {
windowService.sendToAllWindows(I18NChannels.readFileResponse, {
key: readFileArgs.key,
error,
data: typeof data !== 'undefined' && data !== null ? data.toString() : '',
});
});
});
ipcMain.handle(I18NChannels.writeFileRequest, (_event: IpcMainInvokeEvent, writeFileArgs: IWriteFileRequest) => {
const localeFilePath = path.join(LOCALIZATION_FOLDER, writeFileArgs.filename);
const localeFileFolderPath = path.dirname(localeFilePath);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
fs.ensureDir(localeFileFolderPath, (directoryCreationError?: Error) => {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (directoryCreationError) {
console.error(directoryCreationError);
return;
}
fs.writeFile(localeFilePath, JSON.stringify(writeFileArgs.data), (error: Error) => {
windowService.sendToAllWindows(I18NChannels.writeFileResponse, {
keys: writeFileArgs.keys,
error,
});
});
});
});
}
/**
* Clears the bindings from ipcMain;
* in case app is closed/reopened (only on macos)
*/
export function clearMainBindings(ipcMain: IpcMain): void {
ipcMain.removeAllListeners(I18NChannels.readFileRequest);
ipcMain.removeAllListeners(I18NChannels.writeFileRequest);
}

View file

@ -1,57 +1,302 @@
/* eslint-disable unicorn/prevent-abbreviations */
import fs from 'fs-extra';
import path from 'path';
import { IpcMain, BrowserWindow, IpcMainInvokeEvent } from 'electron';
import type { IWindowService } from '@services/windows/interface';
import serviceIdentifier from '@services/serviceIdentifier';
import { container } from '@services/container';
import { LOCALIZATION_FOLDER } from '@services/constants/paths';
import { BackendModule } from 'i18next';
import { cloneDeep, merge } from 'lodash';
import { v4 as uuid } from 'uuid'
import { I18NChannels } from '@/constants/channels';
import { IReadFileRequest, IWriteFileRequest } from './types';
/**
* This is the code that will go into the main.js file
* in order to set up the ipc main bindings
*/
export const mainBindings = function (ipcMain: IpcMain, browserWindow: BrowserWindow): void {
ipcMain.handle(I18NChannels.readFileRequest, (_event: IpcMainInvokeEvent, readFileArgs: IReadFileRequest) => {
const localeFilePath = path.join(LOCALIZATION_FOLDER, readFileArgs.filename);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
fs.readFile(localeFilePath, 'utf8', (error, data) => {
windowService.sendToAllWindows(I18NChannels.readFileResponse, {
key: readFileArgs.key,
error,
data: typeof data !== 'undefined' && data !== null ? data.toString() : '',
// CONFIGS
const defaultOptions = {
debug: false,
loadPath: '/locales/{{lng}}/{{ns}}.json',
addPath: '/locales/{{lng}}/{{ns}}.missing.json',
};
// Merges objects together
const mergeNested = function (object: any, path: string, split: string, value: any) {
const tokens = path.split(split);
let temporary = {};
let temporary2;
temporary[`${tokens[tokens.length - 1]}`] = value;
for (let index = tokens.length - 2; index >= 0; index--) {
temporary2 = {};
temporary2[`${tokens[index]}`] = temporary;
temporary = temporary2;
}
return merge(object, temporary);
};
// https://stackoverflow.com/a/34890276/1837080
const groupByArray = function (xs: any, key: any) {
return xs.reduce(function (rv: any, x: any) {
const v = key instanceof Function ? key(x) : x[key];
const element = rv.find((r: any) => r && r.key === v);
if (element) {
element.values.push(x);
} else {
rv.push({
key: v,
values: [x],
});
});
});
}
return rv;
}, []);
};
// Template is found at: https://www.i18next.com/misc/creating-own-plugins#backend;
// also took code from: https://github.com/i18next/i18next-node-fs-backend
export class Backend implements BackendModule {
static type = 'backend';
type = 'backend' as const;
ipcMain.handle(I18NChannels.writeFileRequest, (_event: IpcMainInvokeEvent, writeFileArgs: IWriteFileRequest) => {
const localeFilePath = path.join(LOCALIZATION_FOLDER, writeFileArgs.filename);
const localeFileFolderPath = path.dirname(localeFilePath);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
fs.ensureDir(localeFileFolderPath, (directoryCreationError?: Error) => {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (directoryCreationError) {
console.error(directoryCreationError);
backendOptions: any;
i18nextOptions: any;
mainLog: any;
readCallbacks: any;
rendererLog: any;
services: any;
useOverflow: any;
writeCallbacks: any;
writeQueue: any;
writeQueueOverflow: any;
writeTimeout: any;
constructor(services: any, backendOptions = {}, i18nextOptions = {}) {
this.init(services, backendOptions, i18nextOptions);
this.readCallbacks = {}; // Callbacks after reading a translation
this.writeCallbacks = {}; // Callbacks after writing a missing translation
this.writeTimeout; // A timer that will initate writing missing translations to files
this.writeQueue = []; // An array to hold missing translations before the writeTimeout occurs
this.writeQueueOverflow = []; // An array to hold missing translations while the writeTimeout's items are being written to file
this.useOverflow = false; // If true, we should insert missing translations into the writeQueueOverflow
}
init(services: any, backendOptions: any, i18nextOptions: any) {
if (typeof window !== 'undefined' && typeof window.i18n.i18nextElectronBackend === 'undefined') {
throw new TypeError("'window.i18n.i18nextElectronBackend' is not defined! Be sure you are setting up your BrowserWindow's preload script properly!");
}
this.services = services;
this.backendOptions = {
...defaultOptions,
...backendOptions,
i18nextElectronBackend: typeof window !== 'undefined' ? window.i18n.i18nextElectronBackend : undefined,
};
this.i18nextOptions = i18nextOptions;
// log-related
const logPrepend = '[i18next-electron-fs-backend:';
this.mainLog = `${logPrepend}main]=>`;
this.rendererLog = `${logPrepend}renderer]=>`;
this.setupIpcBindings();
}
// Sets up Ipc bindings so that we can keep any node-specific
// modules; (ie. 'fs') out of the Electron renderer process
setupIpcBindings() {
const { i18nextElectronBackend } = this.backendOptions;
i18nextElectronBackend.onReceive(I18NChannels.readFileResponse, (arguments_: any) => {
// args:
// {
// key
// error
// data
// }
// Don't know why we need this line;
// upon initialization, the i18next library
// ends up in this .on([channel], args) method twice
if (typeof this.readCallbacks[arguments_.key] === 'undefined') {
return;
}
fs.writeFile(localeFilePath, JSON.stringify(writeFileArgs.data), (error: Error) => {
windowService.sendToAllWindows(I18NChannels.writeFileResponse, {
keys: writeFileArgs.keys,
error,
});
});
let callback;
if (arguments_.error) {
// Failed to read translation file;
// we pass back a fake "success" response
// so that we create a translation file
callback = this.readCallbacks[arguments_.key].callback;
delete this.readCallbacks[arguments_.key];
if (callback !== null && typeof callback === 'function') {
callback(null, {});
}
} else {
let result;
arguments_.data = arguments_.data.replace(/^\uFEFF/, '');
try {
result = JSON.parse(arguments_.data);
} catch (parseError) {
parseError.message = `Error parsing '${arguments_.filename}'. Message: '${parseError}'.`;
callback = this.readCallbacks[arguments_.key].callback;
delete this.readCallbacks[arguments_.key];
if (callback !== null && typeof callback === 'function') {
callback(parseError);
}
return;
}
callback = this.readCallbacks[arguments_.key].callback;
delete this.readCallbacks[arguments_.key];
if (callback !== null && typeof callback === 'function') {
callback(null, result);
}
}
});
});
};
i18nextElectronBackend.onReceive(I18NChannels.writeFileResponse, (arguments_: any) => {
// args:
// {
// keys
// error
// }
const { keys } = arguments_;
for (const key of keys) {
let callback;
// Write methods don't have any callbacks from what I've seen,
// so this is called more than I thought; but necessary!
if (typeof this.writeCallbacks[key] === 'undefined') {
return;
}
if (arguments_.error) {
callback = this.writeCallbacks[key].callback;
delete this.writeCallbacks[key];
callback(arguments_.error);
} else {
callback = this.writeCallbacks[key].callback;
delete this.writeCallbacks[key];
callback(null, true);
}
}
});
}
/**
* Clears the bindings from ipcMain;
* in case app is closed/reopened (only on macos)
*/
export const clearMainBindings = function (ipcMain: IpcMain): void {
ipcMain.removeAllListeners(I18NChannels.readFileRequest);
ipcMain.removeAllListeners(I18NChannels.writeFileRequest);
};
// Writes a given translation to file
write(writeQueue: any) {
const { debug, i18nextElectronBackend } = this.backendOptions;
// Group by filename so we can make one request
// for all changes within a given file
const toWork = groupByArray(writeQueue, 'filename');
for (const element of toWork) {
const anonymous = function (error: any, data: any) {
if (error) {
console.error(
`${this.rendererLog} encountered error when trying to read file '{filename}' before writing missing translation ('{key}'/'{fallbackValue}') to file. Please resolve this error so missing translation values can be written to file. Error: '${error}'.`,
);
return;
}
const keySeparator = !!this.i18nextOptions.keySeparator; // Do we have a key separator or not?
const writeKeys = [];
for (let index = 0; index < element.values.length; index++) {
// If we have no key separator set, simply update the translation value
if (!keySeparator) {
data[element.values[index].key] = element.values[index].fallbackValue;
} else {
// Created the nested object structure based on the key separator, and merge that
// into the existing translation data
data = mergeNested(data, element.values[index].key, this.i18nextOptions.keySeparator, element.values[index].fallbackValue);
}
const writeKey = uuid();
if (element.values[index].callback) {
this.writeCallbacks[writeKey] = {
callback: element.values[index].callback,
};
writeKeys.push(writeKey);
}
}
// Send out the message to the ipcMain process
debug ? console.log(`${this.rendererLog} requesting the missing key '${key}' be written to file '${filename}'.`) : null;
i18nextElectronBackend.send(I18NChannels.writeFileRequest, {
keys: writeKeys,
filename: element.key,
data,
});
}.bind(this);
this.requestFileRead(element.key, anonymous);
}
}
// Reads a given translation file
requestFileRead(filename: any, callback: any) {
const { i18nextElectronBackend } = this.backendOptions;
// Save the callback for this request so we
// can execute once the ipcRender process returns
// with a value from the ipcMain process
const key = uuid();
this.readCallbacks[key] = {
callback,
};
// Send out the message to the ipcMain process
i18nextElectronBackend.send(I18NChannels.readFileRequest, {
key,
filename,
});
}
// Reads a given translation file
read(language: string, namespace: string, callback: any) {
const { loadPath } = this.backendOptions;
const filename = this.services.interpolator.interpolate(loadPath, {
lng: language,
ns: namespace,
});
this.requestFileRead(filename, (error: any, data: any) => {
if (error) {
return callback(error, false);
} // no retry
callback(null, data);
});
}
// Not implementing at this time
readMulti(languages:string, namespaces: any, callback: any) {
throw 'Not implemented exception.';
}
// Writes a missing translation to file
create(languages:string, namespace: any, key: any, fallbackValue: any, callback: any) {
const { addPath } = this.backendOptions;
let filename;
languages = typeof languages === 'string' ? [languages] : languages;
// Create the missing translation for all languages
for (const language of languages) {
filename = this.services.interpolator.interpolate(addPath, {
lng: language,
ns: namespace,
});
// If we are currently writing missing translations from writeQueue,
// temporarily store the requests in writeQueueOverflow until we are
// done writing to file
if (this.useOverflow) {
this.writeQueueOverflow.push({
filename,
key,
fallbackValue,
callback,
});
} else {
this.writeQueue.push({
filename,
key,
fallbackValue,
callback,
});
}
}
// Fire up the timeout to process items to write
if (this.writeQueue.length > 0 && !this.useOverflow) {
// Clear out any existing timeout if we are still getting translations to write
if (typeof this.writeTimeout !== 'undefined') {
clearInterval(this.writeTimeout);
}
this.writeTimeout = setInterval(
function () {
// Write writeQueue entries, then after,
// fill in any from the writeQueueOverflow
if (this.writeQueue.length > 0) {
this.write(cloneDeep(this.writeQueue));
}
this.writeQueue = cloneDeep(this.writeQueueOverflow);
this.writeQueueOverflow = [];
if (this.writeQueue.length === 0) {
// Clear timer
clearInterval(this.writeTimeout);
delete this.writeTimeout;
this.useOverflow = false;
}
}.bind(this),
1000,
);
this.useOverflow = true;
}
}
}

View file

@ -4,8 +4,8 @@ import Backend from 'i18next-fs-backend';
import isDevelopment from 'electron-is-dev';
import { LOCALIZATION_FOLDER } from '@services/constants/paths';
import bindI18nListener from './bindI18nListener';
import changeToDefaultLanguage from './useDefaultLanguage';
import { mainBindings } from './i18nMainBindings';
// init i18n is async, but our usage is basically await the electron app to start, so this is basically ok
void i18next.use(Backend).init({
@ -23,8 +23,8 @@ void i18next.use(Backend).init({
fallbackLng: isDevelopment ? false : 'en', // set to false when generating translation files locally
});
export async function initI18NAfterServiceReady(): Promise<void> {
await bindI18nListener();
export async function initRendererI18NHandler(): Promise<void> {
mainBindings();
await changeToDefaultLanguage(i18next);
}

View file

@ -178,10 +178,10 @@ export class Window implements IWindowService {
newWindow.on('closed', () => {
this.windows[windowName] = undefined;
});
await newWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
let webContentLoadingPromise: Promise<void> | undefined;
if (isMainWindow) {
// handle window show and Webview/browserView show
return await new Promise<void>((resolve) => {
webContentLoadingPromise = new Promise<void>((resolve) => {
newWindow.once('ready-to-show', () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow === undefined) return;
@ -202,6 +202,9 @@ export class Window implements IWindowService {
});
});
}
// This loading will wait for a while
await newWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
await webContentLoadingPromise;
}
private registerMainWindowListeners(newWindow: BrowserWindow): void {