Move to smartling for translation services
This commit is contained in:
parent
620067342a
commit
5957c111cf
|
@ -17,6 +17,7 @@ release/
|
|||
/start.sh
|
||||
.eslintcache
|
||||
tsconfig.tsbuildinfo
|
||||
.smartling-source.sh
|
||||
|
||||
# generated files
|
||||
js/components.js
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/bash
|
||||
# Copyright 2022 Signal Messenger, LLC
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
# run this before yarn get-strings/push-strings:
|
||||
# source .smartling-source.sh
|
||||
|
||||
export SMARTLING_USER="your token 'user identifier' here"
|
||||
export SMARTLING_SECRET="your token secret here"
|
|
@ -0,0 +1,7 @@
|
|||
# Copyright 2022 Signal Messenger, LLC
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
# https://github.com/Smartling/smartling-cli/wiki/examples.md
|
||||
|
||||
account_id: '92ff14ad'
|
||||
project_id: 'ef62d1ebb'
|
|
@ -1,9 +0,0 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:signalapp:p:signal-desktop:r:messagesjson-electron]
|
||||
file_filter = _locales/<lang>/messages.json
|
||||
source_file = _locales/en/messages.json
|
||||
source_lang = en
|
||||
type = CHROME
|
||||
|
|
@ -272,15 +272,3 @@ yarn build
|
|||
```
|
||||
|
||||
Then, run the tests using `yarn test-release`.
|
||||
|
||||
## Translations
|
||||
|
||||
To pull the latest translations, follow these steps:
|
||||
|
||||
1. Download Transifex client:
|
||||
https://docs.transifex.com/client/installing-the-client
|
||||
2. Create Transifex account: https://transifex.com
|
||||
3. Generate API token: https://www.transifex.com/user/settings/api/
|
||||
4. Create `~/.transifexrc` configuration:
|
||||
https://docs.transifex.com/client/client-configuration#-transifexrc
|
||||
5. Run `yarn get-strings`.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,14 @@
|
|||
{
|
||||
"smartling": {
|
||||
"placeholder_format_custom": "(\\$.+?\\$)",
|
||||
"translate_paths": [
|
||||
{
|
||||
"key": "{*}/message",
|
||||
"path": "*/message",
|
||||
"instruction": "*/description"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AddUserToAnotherGroupModal__title": {
|
||||
"message": "Add to a group",
|
||||
"description": "Shown as the title of the dialog that allows you to add a contact to an group"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -5,10 +5,10 @@ import { join } from 'path';
|
|||
import type { BrowserWindow, NativeImage } from 'electron';
|
||||
import { Menu, Tray, app, nativeImage } from 'electron';
|
||||
import * as log from '../ts/logging/log';
|
||||
import type { LocaleMessagesType } from '../ts/types/I18N';
|
||||
import type { LocalizerType } from '../ts/types/I18N';
|
||||
|
||||
export type SystemTrayServiceOptionsType = Readonly<{
|
||||
messages: LocaleMessagesType;
|
||||
i18n: LocalizerType;
|
||||
|
||||
// For testing
|
||||
createTrayInstance?: (icon: NativeImage) => Tray;
|
||||
|
@ -24,7 +24,7 @@ export type SystemTrayServiceOptionsType = Readonly<{
|
|||
export class SystemTrayService {
|
||||
private browserWindow?: BrowserWindow;
|
||||
|
||||
private readonly messages: LocaleMessagesType;
|
||||
private readonly i18n: LocalizerType;
|
||||
|
||||
private tray?: Tray;
|
||||
|
||||
|
@ -38,9 +38,9 @@ export class SystemTrayService {
|
|||
|
||||
private createTrayInstance: (icon: NativeImage) => Tray;
|
||||
|
||||
constructor({ messages, createTrayInstance }: SystemTrayServiceOptionsType) {
|
||||
constructor({ i18n, createTrayInstance }: SystemTrayServiceOptionsType) {
|
||||
log.info('System tray service: created');
|
||||
this.messages = messages;
|
||||
this.i18n = i18n;
|
||||
this.boundRender = this.render.bind(this);
|
||||
this.createTrayInstance = createTrayInstance || (icon => new Tray(icon));
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ export class SystemTrayService {
|
|||
id: 'toggleWindowVisibility',
|
||||
...(browserWindow?.isVisible()
|
||||
? {
|
||||
label: this.messages.hide.message,
|
||||
label: this.i18n('hide'),
|
||||
click: () => {
|
||||
log.info(
|
||||
'System tray service: hiding the window from the context menu'
|
||||
|
@ -167,7 +167,7 @@ export class SystemTrayService {
|
|||
},
|
||||
}
|
||||
: {
|
||||
label: this.messages.show.message,
|
||||
label: this.i18n('show'),
|
||||
click: () => {
|
||||
log.info(
|
||||
'System tray service: showing the window from the context menu'
|
||||
|
@ -181,7 +181,7 @@ export class SystemTrayService {
|
|||
},
|
||||
{
|
||||
id: 'quit',
|
||||
label: this.messages.quit.message,
|
||||
label: this.i18n('quit'),
|
||||
click: () => {
|
||||
log.info(
|
||||
'System tray service: quitting the app from the context menu'
|
||||
|
@ -225,7 +225,7 @@ export class SystemTrayService {
|
|||
forceOnTop(browserWindow);
|
||||
});
|
||||
|
||||
result.setToolTip(this.messages.signalDesktop.message);
|
||||
result.setToolTip(this.i18n('signalDesktop'));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@ import { app, dialog, clipboard } from 'electron';
|
|||
|
||||
import * as Errors from '../ts/types/errors';
|
||||
import { redactAll } from '../ts/util/privacy';
|
||||
import type { LocaleMessagesType } from '../ts/types/I18N';
|
||||
import { reallyJsonStringify } from '../ts/util/reallyJsonStringify';
|
||||
import type { LocaleType } from './locale';
|
||||
|
||||
// We use hard-coded strings until we're able to update these strings from the locale.
|
||||
let quitText = 'Quit';
|
||||
|
@ -39,9 +39,9 @@ function handleError(prefix: string, error: Error): void {
|
|||
app.exit(1);
|
||||
}
|
||||
|
||||
export const updateLocale = (messages: LocaleMessagesType): void => {
|
||||
quitText = messages.quit.message;
|
||||
copyErrorAndQuitText = messages.copyErrorAndQuit.message;
|
||||
export const updateLocale = (locale: LocaleType): void => {
|
||||
quitText = locale.i18n('quit');
|
||||
copyErrorAndQuitText = locale.i18n('copyErrorAndQuit');
|
||||
};
|
||||
|
||||
function _getError(reason: unknown): Error {
|
||||
|
|
|
@ -20,15 +20,7 @@ function removeRegion(locale: string): string {
|
|||
}
|
||||
|
||||
function getLocaleMessages(locale: string): LocaleMessagesType {
|
||||
const onDiskLocale = locale.replace('-', '_');
|
||||
|
||||
const targetFile = join(
|
||||
__dirname,
|
||||
'..',
|
||||
'_locales',
|
||||
onDiskLocale,
|
||||
'messages.json'
|
||||
);
|
||||
const targetFile = join(__dirname, '..', '_locales', locale, 'messages.json');
|
||||
|
||||
return JSON.parse(readFileSync(targetFile, 'utf-8'));
|
||||
}
|
||||
|
@ -81,7 +73,7 @@ export function load({
|
|||
//
|
||||
// possible locales:
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/base/l10n/l10n_util.cc
|
||||
const normalized = removeRegion(appLocale);
|
||||
const languageOnly = removeRegion(appLocale);
|
||||
|
||||
try {
|
||||
return finalize(getLocaleMessages(appLocale), english, appLocale);
|
||||
|
@ -90,11 +82,11 @@ export function load({
|
|||
}
|
||||
|
||||
try {
|
||||
logger.warn(`Falling back to parent language: '${normalized}'`);
|
||||
logger.warn(`Falling back to parent language: '${languageOnly}'`);
|
||||
// Note: messages are from parent language, but we still keep the region
|
||||
return finalize(getLocaleMessages(normalized), english, appLocale);
|
||||
return finalize(getLocaleMessages(languageOnly), english, appLocale);
|
||||
} catch (e) {
|
||||
logger.error(`Problem loading messages for locale ${normalized}`);
|
||||
logger.error(`Problem loading messages for locale ${languageOnly}`);
|
||||
|
||||
logger.warn("Falling back to 'en' locale");
|
||||
return finalize(english, english, 'en');
|
||||
|
|
|
@ -1740,7 +1740,7 @@ app.on('ready', async () => {
|
|||
);
|
||||
}
|
||||
|
||||
GlobalErrors.updateLocale(locale.messages);
|
||||
GlobalErrors.updateLocale(locale);
|
||||
|
||||
// If the sql initialization takes more than three seconds to complete, we
|
||||
// want to notify the user that things are happening
|
||||
|
@ -1888,7 +1888,7 @@ app.on('ready', async () => {
|
|||
|
||||
setupMenu();
|
||||
|
||||
systemTrayService = new SystemTrayService({ messages: locale.messages });
|
||||
systemTrayService = new SystemTrayService({ i18n: locale.i18n });
|
||||
systemTrayService.setMainWindow(mainWindow);
|
||||
systemTrayService.setEnabled(
|
||||
shouldMinimizeToSystemTray(await systemTraySettingCache.get())
|
||||
|
@ -1931,7 +1931,7 @@ function setupMenu(options?: Partial<CreateTemplateOptionsType>) {
|
|||
// overrides
|
||||
...options,
|
||||
};
|
||||
const template = createTemplate(menuOptions, getLocale().messages);
|
||||
const template = createTemplate(menuOptions, getLocale().i18n);
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
|
|
106
app/menu.ts
106
app/menu.ts
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { isString } from 'lodash';
|
||||
|
||||
import type { LocaleMessagesType } from '../ts/types/I18N';
|
||||
import type { LocalizerType } from '../ts/types/I18N';
|
||||
import type {
|
||||
MenuListType,
|
||||
MenuOptionsType,
|
||||
|
@ -14,7 +14,7 @@ export type CreateTemplateOptionsType = MenuOptionsType & MenuActionsType;
|
|||
|
||||
export const createTemplate = (
|
||||
options: CreateTemplateOptionsType,
|
||||
messages: LocaleMessagesType
|
||||
i18n: LocalizerType
|
||||
): MenuListType => {
|
||||
if (!isString(options.platform)) {
|
||||
throw new TypeError('`options.platform` must be a string');
|
||||
|
@ -42,14 +42,14 @@ export const createTemplate = (
|
|||
|
||||
const template: MenuListType = [
|
||||
{
|
||||
label: messages.mainMenuFile.message,
|
||||
label: i18n('mainMenuFile'),
|
||||
submenu: [
|
||||
{
|
||||
label: messages.mainMenuCreateStickers.message,
|
||||
label: i18n('mainMenuCreateStickers'),
|
||||
click: showStickerCreator,
|
||||
},
|
||||
{
|
||||
label: messages.mainMenuSettings.message,
|
||||
label: i18n('mainMenuSettings'),
|
||||
accelerator: 'CommandOrControl+,',
|
||||
click: showSettings,
|
||||
},
|
||||
|
@ -58,78 +58,78 @@ export const createTemplate = (
|
|||
},
|
||||
{
|
||||
role: 'quit',
|
||||
label: messages.appMenuQuit.message,
|
||||
label: i18n('appMenuQuit'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: messages.mainMenuEdit.message,
|
||||
label: i18n('mainMenuEdit'),
|
||||
submenu: [
|
||||
{
|
||||
role: 'undo',
|
||||
label: messages.editMenuUndo.message,
|
||||
label: i18n('editMenuUndo'),
|
||||
},
|
||||
{
|
||||
role: 'redo',
|
||||
label: messages.editMenuRedo.message,
|
||||
label: i18n('editMenuRedo'),
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
role: 'cut',
|
||||
label: messages.editMenuCut.message,
|
||||
label: i18n('editMenuCut'),
|
||||
},
|
||||
{
|
||||
role: 'copy',
|
||||
label: messages.editMenuCopy.message,
|
||||
label: i18n('editMenuCopy'),
|
||||
},
|
||||
{
|
||||
role: 'paste',
|
||||
label: messages.editMenuPaste.message,
|
||||
label: i18n('editMenuPaste'),
|
||||
},
|
||||
{
|
||||
role: 'pasteAndMatchStyle',
|
||||
label: messages.editMenuPasteAndMatchStyle.message,
|
||||
label: i18n('editMenuPasteAndMatchStyle'),
|
||||
},
|
||||
{
|
||||
role: 'delete',
|
||||
label: messages.editMenuDelete.message,
|
||||
label: i18n('editMenuDelete'),
|
||||
},
|
||||
{
|
||||
role: 'selectAll',
|
||||
label: messages.editMenuSelectAll.message,
|
||||
label: i18n('editMenuSelectAll'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: messages.mainMenuView.message,
|
||||
label: i18n('mainMenuView'),
|
||||
submenu: [
|
||||
{
|
||||
role: 'resetZoom',
|
||||
label: messages.viewMenuResetZoom.message,
|
||||
label: i18n('viewMenuResetZoom'),
|
||||
},
|
||||
{
|
||||
accelerator: 'CmdOrCtrl+=',
|
||||
role: 'zoomIn',
|
||||
label: messages.viewMenuZoomIn.message,
|
||||
label: i18n('viewMenuZoomIn'),
|
||||
},
|
||||
{
|
||||
role: 'zoomOut',
|
||||
label: messages.viewMenuZoomOut.message,
|
||||
label: i18n('viewMenuZoomOut'),
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
role: 'togglefullscreen',
|
||||
label: messages.viewMenuToggleFullScreen.message,
|
||||
label: i18n('viewMenuToggleFullScreen'),
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: messages.debugLog.message,
|
||||
label: i18n('debugLog'),
|
||||
click: showDebugLog,
|
||||
},
|
||||
...(devTools
|
||||
|
@ -139,14 +139,14 @@ export const createTemplate = (
|
|||
},
|
||||
{
|
||||
role: 'toggleDevTools' as const,
|
||||
label: messages.viewMenuToggleDevTools.message,
|
||||
label: i18n('viewMenuToggleDevTools'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(devTools && platform !== 'linux'
|
||||
? [
|
||||
{
|
||||
label: messages.forceUpdate.message,
|
||||
label: i18n('forceUpdate'),
|
||||
click: forceUpdate,
|
||||
},
|
||||
]
|
||||
|
@ -154,21 +154,21 @@ export const createTemplate = (
|
|||
],
|
||||
},
|
||||
{
|
||||
label: messages.mainMenuWindow.message,
|
||||
label: i18n('mainMenuWindow'),
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{
|
||||
role: 'minimize',
|
||||
label: messages.windowMenuMinimize.message,
|
||||
label: i18n('windowMenuMinimize'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: messages.mainMenuHelp.message,
|
||||
label: i18n('mainMenuHelp'),
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: messages.helpMenuShowKeyboardShortcuts.message,
|
||||
label: i18n('helpMenuShowKeyboardShortcuts'),
|
||||
accelerator: 'CmdOrCtrl+/',
|
||||
click: showKeyboardShortcuts,
|
||||
},
|
||||
|
@ -176,25 +176,25 @@ export const createTemplate = (
|
|||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: messages.contactUs.message,
|
||||
label: i18n('contactUs'),
|
||||
click: openContactUs,
|
||||
},
|
||||
{
|
||||
label: messages.goToReleaseNotes.message,
|
||||
label: i18n('goToReleaseNotes'),
|
||||
click: openReleaseNotes,
|
||||
},
|
||||
{
|
||||
label: messages.goToForums.message,
|
||||
label: i18n('goToForums'),
|
||||
click: openForums,
|
||||
},
|
||||
{
|
||||
label: messages.goToSupportPage.message,
|
||||
label: i18n('goToSupportPage'),
|
||||
click: openSupportPage,
|
||||
},
|
||||
...(isProduction
|
||||
? [
|
||||
{
|
||||
label: messages.joinTheBeta.message,
|
||||
label: i18n('joinTheBeta'),
|
||||
click: openJoinTheBeta,
|
||||
},
|
||||
]
|
||||
|
@ -203,7 +203,7 @@ export const createTemplate = (
|
|||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: messages.aboutSignalDesktop.message,
|
||||
label: i18n('aboutSignalDesktop'),
|
||||
click: showAbout,
|
||||
},
|
||||
],
|
||||
|
@ -217,7 +217,7 @@ export const createTemplate = (
|
|||
// These are in reverse order, since we're prepending them one at a time
|
||||
if (options.development) {
|
||||
fileMenu.submenu.unshift({
|
||||
label: messages.menuSetupAsStandalone.message,
|
||||
label: i18n('menuSetupAsStandalone'),
|
||||
click: setupAsStandalone,
|
||||
});
|
||||
}
|
||||
|
@ -226,7 +226,7 @@ export const createTemplate = (
|
|||
type: 'separator',
|
||||
});
|
||||
fileMenu.submenu.unshift({
|
||||
label: messages.menuSetupAsNewDevice.message,
|
||||
label: i18n('menuSetupAsNewDevice'),
|
||||
click: setupAsNewDevice,
|
||||
});
|
||||
} else {
|
||||
|
@ -235,7 +235,7 @@ export const createTemplate = (
|
|||
}
|
||||
|
||||
if (platform === 'darwin') {
|
||||
return updateForMac(template, messages, options);
|
||||
return updateForMac(template, i18n, options);
|
||||
}
|
||||
|
||||
return template;
|
||||
|
@ -243,7 +243,7 @@ export const createTemplate = (
|
|||
|
||||
function updateForMac(
|
||||
template: MenuListType,
|
||||
messages: LocaleMessagesType,
|
||||
i18n: LocalizerType,
|
||||
options: CreateTemplateOptionsType
|
||||
): MenuListType {
|
||||
const { showAbout, showSettings, showWindow } = options;
|
||||
|
@ -270,7 +270,7 @@ function updateForMac(
|
|||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: messages.windowMenuClose.message,
|
||||
label: i18n('windowMenuClose'),
|
||||
accelerator: 'CmdOrCtrl+W',
|
||||
role: 'close',
|
||||
}
|
||||
|
@ -281,17 +281,17 @@ function updateForMac(
|
|||
|
||||
// Add the OSX-specific Signal Desktop menu at the far left
|
||||
template.unshift({
|
||||
label: messages.signalDesktop.message,
|
||||
label: i18n('signalDesktop'),
|
||||
submenu: [
|
||||
{
|
||||
label: messages.aboutSignalDesktop.message,
|
||||
label: i18n('aboutSignalDesktop'),
|
||||
click: showAbout,
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: messages.mainMenuSettings.message,
|
||||
label: i18n('mainMenuSettings'),
|
||||
accelerator: 'CommandOrControl+,',
|
||||
click: showSettings,
|
||||
},
|
||||
|
@ -299,29 +299,29 @@ function updateForMac(
|
|||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: messages.appMenuServices.message,
|
||||
label: i18n('appMenuServices'),
|
||||
role: 'services',
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: messages.appMenuHide.message,
|
||||
label: i18n('appMenuHide'),
|
||||
role: 'hide',
|
||||
},
|
||||
{
|
||||
label: messages.appMenuHideOthers.message,
|
||||
label: i18n('appMenuHideOthers'),
|
||||
role: 'hideOthers',
|
||||
},
|
||||
{
|
||||
label: messages.appMenuUnhide.message,
|
||||
label: i18n('appMenuUnhide'),
|
||||
role: 'unhide',
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: messages.appMenuQuit.message,
|
||||
label: i18n('appMenuQuit'),
|
||||
role: 'quit',
|
||||
},
|
||||
],
|
||||
|
@ -334,15 +334,15 @@ function updateForMac(
|
|||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: messages.speech.message,
|
||||
label: i18n('speech'),
|
||||
submenu: [
|
||||
{
|
||||
role: 'startSpeaking',
|
||||
label: messages.editMenuStartSpeaking.message,
|
||||
label: i18n('editMenuStartSpeaking'),
|
||||
},
|
||||
{
|
||||
role: 'stopSpeaking',
|
||||
label: messages.editMenuStopSpeaking.message,
|
||||
label: i18n('editMenuStopSpeaking'),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@ -355,16 +355,16 @@ function updateForMac(
|
|||
// eslint-disable-next-line no-param-reassign
|
||||
template[4].submenu = [
|
||||
{
|
||||
label: messages.windowMenuMinimize.message,
|
||||
label: i18n('windowMenuMinimize'),
|
||||
accelerator: 'CmdOrCtrl+M',
|
||||
role: 'minimize',
|
||||
},
|
||||
{
|
||||
label: messages.windowMenuZoom.message,
|
||||
label: i18n('windowMenuZoom'),
|
||||
role: 'zoom',
|
||||
},
|
||||
{
|
||||
label: messages.show.message,
|
||||
label: i18n('show'),
|
||||
accelerator: 'CmdOrCtrl+Shift+0',
|
||||
click: showWindow,
|
||||
},
|
||||
|
@ -373,7 +373,7 @@ function updateForMac(
|
|||
},
|
||||
{
|
||||
role: 'front',
|
||||
label: messages.windowMenuBringAllToFront.message,
|
||||
label: i18n('windowMenuBringAllToFront'),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ export function getLanguages(
|
|||
|
||||
export const setup = (
|
||||
browserWindow: BrowserWindow,
|
||||
{ name: userLocale, messages }: LocaleType
|
||||
{ name: userLocale, i18n }: LocaleType
|
||||
): void => {
|
||||
const { session } = browserWindow.webContents;
|
||||
const availableLocales = session.availableSpellCheckerLanguages;
|
||||
|
@ -68,7 +68,7 @@ export const setup = (
|
|||
);
|
||||
} else {
|
||||
template.push({
|
||||
label: messages.contextMenuNoSuggestions.message,
|
||||
label: i18n('contextMenuNoSuggestions'),
|
||||
enabled: false,
|
||||
});
|
||||
}
|
||||
|
@ -77,18 +77,18 @@ export const setup = (
|
|||
|
||||
if (params.isEditable) {
|
||||
if (editFlags.canUndo) {
|
||||
template.push({ label: messages.editMenuUndo.message, role: 'undo' });
|
||||
template.push({ label: i18n('editMenuUndo'), role: 'undo' });
|
||||
}
|
||||
// This is only ever `true` if undo was triggered via the context menu
|
||||
// (not ctrl/cmd+z)
|
||||
if (editFlags.canRedo) {
|
||||
template.push({ label: messages.editMenuRedo.message, role: 'redo' });
|
||||
template.push({ label: i18n('editMenuRedo'), role: 'redo' });
|
||||
}
|
||||
if (editFlags.canUndo || editFlags.canRedo) {
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
if (editFlags.canCut) {
|
||||
template.push({ label: messages.editMenuCut.message, role: 'cut' });
|
||||
template.push({ label: i18n('editMenuCut'), role: 'cut' });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,7 +100,7 @@ export const setup = (
|
|||
click = () => {
|
||||
clipboard.writeText(params.linkURL);
|
||||
};
|
||||
label = messages.contextMenuCopyLink.message;
|
||||
label = i18n('contextMenuCopyLink');
|
||||
} else if (isImage) {
|
||||
click = () => {
|
||||
const parsedSrcUrl = maybeParseUrl(params.srcURL);
|
||||
|
@ -113,9 +113,9 @@ export const setup = (
|
|||
);
|
||||
clipboard.writeImage(image);
|
||||
};
|
||||
label = messages.contextMenuCopyImage.message;
|
||||
label = i18n('contextMenuCopyImage');
|
||||
} else {
|
||||
label = messages.editMenuCopy.message;
|
||||
label = i18n('editMenuCopy');
|
||||
}
|
||||
|
||||
template.push({
|
||||
|
@ -126,12 +126,12 @@ export const setup = (
|
|||
}
|
||||
|
||||
if (editFlags.canPaste && !isImage) {
|
||||
template.push({ label: messages.editMenuPaste.message, role: 'paste' });
|
||||
template.push({ label: i18n('editMenuPaste'), role: 'paste' });
|
||||
}
|
||||
|
||||
if (editFlags.canPaste && !isImage) {
|
||||
template.push({
|
||||
label: messages.editMenuPasteAndMatchStyle.message,
|
||||
label: i18n('editMenuPasteAndMatchStyle'),
|
||||
role: 'pasteAndMatchStyle',
|
||||
});
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ export const setup = (
|
|||
// results in all the UI being selected
|
||||
if (editFlags.canSelectAll && params.isEditable) {
|
||||
template.push({
|
||||
label: messages.editMenuSelectAll.message,
|
||||
label: i18n('editMenuSelectAll'),
|
||||
role: 'selectAll',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"sign-release": "node ts/updater/generateSignature.js",
|
||||
"notarize": "echo 'No longer necessary'",
|
||||
"get-strings": "node ts/scripts/get-strings.js",
|
||||
"push-strings": "node ts/scripts/push-strings.js",
|
||||
"get-expire-time": "node ts/scripts/get-expire-time.js",
|
||||
"copy-and-concat": "node ts/scripts/copy-and-concat.js",
|
||||
"sass": "sass stylesheets/manifest.scss:stylesheets/manifest.css stylesheets/manifest_bridge.scss:stylesheets/manifest_bridge.css",
|
||||
|
|
|
@ -7,7 +7,6 @@ import { Globals } from '@react-spring/web';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import type { ExecuteMenuRoleType } from './TitleBarContainer';
|
||||
import type { LocaleMessagesType } from '../types/I18N';
|
||||
import type { MenuOptionsType, MenuActionType } from '../types/menu';
|
||||
import type { ToastType } from '../state/ducks/toast';
|
||||
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||
|
@ -24,7 +23,6 @@ import { useReducedMotion } from '../hooks/useReducedMotion';
|
|||
|
||||
type PropsType = {
|
||||
appView: AppViewType;
|
||||
localeMessages: LocaleMessagesType;
|
||||
openInbox: () => void;
|
||||
registerSingleDevice: (number: string, code: string) => Promise<void>;
|
||||
renderCallManager: () => JSX.Element;
|
||||
|
@ -71,7 +69,6 @@ export const App = ({
|
|||
isMaximized,
|
||||
isShowingStoriesView,
|
||||
hasCustomTitleBar,
|
||||
localeMessages,
|
||||
menuOptions,
|
||||
openInbox,
|
||||
registerSingleDevice,
|
||||
|
@ -163,7 +160,7 @@ export const App = ({
|
|||
titleBarDoubleClick={titleBarDoubleClick}
|
||||
hasMenu
|
||||
hideMenuBar={hideMenuBar}
|
||||
localeMessages={localeMessages}
|
||||
i18n={i18n}
|
||||
menuOptions={menuOptions}
|
||||
executeMenuAction={executeMenuAction}
|
||||
>
|
||||
|
|
|
@ -10,13 +10,13 @@ import classNames from 'classnames';
|
|||
|
||||
import { createTemplate } from '../../app/menu';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import type { LocaleMessagesType } from '../types/I18N';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import type { MenuOptionsType, MenuActionType } from '../types/menu';
|
||||
import { useIsWindowActive } from '../hooks/useIsWindowActive';
|
||||
|
||||
export type MenuPropsType = Readonly<{
|
||||
hasMenu: true;
|
||||
localeMessages: LocaleMessagesType;
|
||||
i18n: LocalizerType;
|
||||
menuOptions: MenuOptionsType;
|
||||
executeMenuAction: (action: MenuActionType) => void;
|
||||
}>;
|
||||
|
@ -64,7 +64,7 @@ ROLE_TO_ACCELERATOR.set('minimize', 'CmdOrCtrl+M');
|
|||
function convertMenu(
|
||||
menuList: ReadonlyArray<MenuItemConstructorOptions>,
|
||||
executeMenuRole: (role: MenuItemConstructorOptions['role']) => void,
|
||||
localeMessages: LocaleMessagesType
|
||||
i18n: LocalizerType
|
||||
): Array<MenuItem> {
|
||||
return menuList.map(item => {
|
||||
const {
|
||||
|
@ -78,7 +78,7 @@ function convertMenu(
|
|||
let submenu: Array<MenuItem> | undefined;
|
||||
|
||||
if (Array.isArray(originalSubmenu)) {
|
||||
submenu = convertMenu(originalSubmenu, executeMenuRole, localeMessages);
|
||||
submenu = convertMenu(originalSubmenu, executeMenuRole, i18n);
|
||||
} else if (originalSubmenu) {
|
||||
throw new Error('Non-array submenu is not supported');
|
||||
}
|
||||
|
@ -107,12 +107,9 @@ function convertMenu(
|
|||
// `app/main.ts`.
|
||||
accelerator = accelerator?.replace(
|
||||
/CommandOrControl|CmdOrCtrl/g,
|
||||
localeMessages['Keyboard--Key--ctrl'].message
|
||||
);
|
||||
accelerator = accelerator?.replace(
|
||||
/Shift/g,
|
||||
localeMessages['Keyboard--Key--shift'].message
|
||||
i18n('Keyboard--Key--ctrl')
|
||||
);
|
||||
accelerator = accelerator?.replace(/Shift/g, i18n('Keyboard--Key--shift'));
|
||||
|
||||
return {
|
||||
type,
|
||||
|
@ -221,7 +218,7 @@ export const TitleBarContainer = (props: PropsType): JSX.Element => {
|
|||
|
||||
let maybeMenu: Array<MenuItem> | undefined;
|
||||
if (hasMenu) {
|
||||
const { localeMessages, menuOptions, executeMenuAction } = props;
|
||||
const { i18n, menuOptions, executeMenuAction } = props;
|
||||
|
||||
const menuTemplate = createTemplate(
|
||||
{
|
||||
|
@ -243,10 +240,10 @@ export const TitleBarContainer = (props: PropsType): JSX.Element => {
|
|||
showStickerCreator: () => executeMenuAction('showStickerCreator'),
|
||||
showWindow: () => executeMenuAction('showWindow'),
|
||||
},
|
||||
localeMessages
|
||||
i18n
|
||||
);
|
||||
|
||||
maybeMenu = convertMenu(menuTemplate, executeMenuRole, localeMessages);
|
||||
maybeMenu = convertMenu(menuTemplate, executeMenuRole, i18n);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -19,10 +19,10 @@ export async function afterPack({
|
|||
if (electronPlatformName === 'darwin') {
|
||||
const { productFilename } = packager.appInfo;
|
||||
|
||||
// en.lproj/locale.pak
|
||||
// zh_CN.lproj/locale.pak
|
||||
// en.lproj/*
|
||||
// zh_CN.lproj/*
|
||||
defaultLocale = 'en.lproj';
|
||||
ourLocales = ourLocales.map(locale => `${locale}.lproj`);
|
||||
ourLocales = ourLocales.map(locale => `${locale.replace(/-/g, '_')}.lproj`);
|
||||
|
||||
localesPath = path.join(
|
||||
appOutDir,
|
||||
|
@ -35,6 +35,8 @@ export async function afterPack({
|
|||
electronPlatformName === 'win32'
|
||||
) {
|
||||
// Shared between windows and linux
|
||||
// en-US.pak
|
||||
// zh-CN.pak
|
||||
defaultLocale = 'en-US.pak';
|
||||
ourLocales = ourLocales.map(locale => {
|
||||
if (locale === 'en') {
|
||||
|
|
|
@ -1,76 +1,36 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { join, resolve } from 'path';
|
||||
import { existsSync, readdirSync, writeFileSync } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import { readJsonSync } from 'fs-extra';
|
||||
import type { LocaleMessagesType } from '../types/I18N';
|
||||
import * as Errors from '../types/errors';
|
||||
const { SMARTLING_USER, SMARTLING_SECRET } = process.env;
|
||||
|
||||
console.log('Getting latest strings!');
|
||||
|
||||
// Note: we continue after tx failures so we always restore placeholders on json files
|
||||
let failed = false;
|
||||
|
||||
console.log();
|
||||
console.log('Getting strings, allow for new ones over 80% translated');
|
||||
try {
|
||||
execSync('tx pull --all --use-git-timestamps --minimum-perc=80', {
|
||||
stdio: [null, process.stdout, process.stderr],
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
failed = true;
|
||||
console.log(
|
||||
'Failed first tx fetch, continuing...',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
if (!SMARTLING_USER) {
|
||||
console.error('Need to set SMARTLING_USER environment variable!');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!SMARTLING_SECRET) {
|
||||
console.error('Need to set SMARTLING_SECRET environment variable!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Fetching latest strings!');
|
||||
console.log();
|
||||
console.log('Getting strings, updating everything previously missed');
|
||||
try {
|
||||
execSync('tx pull --use-git-timestamps', {
|
||||
execSync(
|
||||
'smartling-cli' +
|
||||
` --user "${SMARTLING_USER}"` +
|
||||
` --secret "${SMARTLING_SECRET}"` +
|
||||
' --config .smartling.yml' +
|
||||
' --verbose' +
|
||||
' --format "_locales/{{.Locale}}/messages.json"' +
|
||||
' files pull',
|
||||
{
|
||||
stdio: [null, process.stdout, process.stderr],
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
failed = true;
|
||||
console.log(
|
||||
'Failed second tx fetch, continuing...',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
|
||||
const BASE_DIR = join(__dirname, '../../_locales');
|
||||
const locales = readdirSync(join(BASE_DIR, ''));
|
||||
|
||||
console.log();
|
||||
console.log('Deleting placeholders for all locales');
|
||||
locales.forEach((locale: string) => {
|
||||
const target = resolve(join(BASE_DIR, locale, 'messages.json'));
|
||||
if (!existsSync(target)) {
|
||||
console.warn(`File not found for ${locale}: ${target}`);
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
const messages: LocaleMessagesType = readJsonSync(target);
|
||||
Object.keys(messages).forEach(key => {
|
||||
delete messages[key].placeholders;
|
||||
|
||||
if (!messages[key].description) {
|
||||
delete messages[key].description;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Writing ${target}`);
|
||||
writeFileSync(target, `${JSON.stringify(messages, null, 4)}\n`);
|
||||
});
|
||||
|
||||
console.log('Formatting newly-downloaded strings!');
|
||||
console.log();
|
||||
execSync('yarn format', {
|
||||
stdio: [null, process.stdout, process.stderr],
|
||||
});
|
||||
|
||||
if (failed) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const { SMARTLING_USER, SMARTLING_SECRET } = process.env;
|
||||
|
||||
if (!SMARTLING_USER) {
|
||||
console.error('Need to set SMARTLING_USER environment variable!');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!SMARTLING_SECRET) {
|
||||
console.error('Need to set SMARTLING_SECRET environment variable!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Pushing latest strings!');
|
||||
console.log();
|
||||
execSync(
|
||||
'smartling-cli' +
|
||||
` --user "${SMARTLING_USER}"` +
|
||||
` --secret "${SMARTLING_SECRET}"` +
|
||||
' --config .smartling.yml' +
|
||||
' --verbose' +
|
||||
' files push _locales/en/messages.json',
|
||||
{
|
||||
stdio: [null, process.stdout, process.stderr],
|
||||
}
|
||||
);
|
|
@ -7,9 +7,9 @@ import { setupI18n } from '../../util/setupI18n';
|
|||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import esMessages from '../../../_locales/es/messages.json';
|
||||
import nbMessages from '../../../_locales/nb/messages.json';
|
||||
import nnMessages from '../../../_locales/nn/messages.json';
|
||||
import ptBrMessages from '../../../_locales/pt_BR/messages.json';
|
||||
import zhCnMessages from '../../../_locales/zh_CN/messages.json';
|
||||
import nlMessages from '../../../_locales/nl/messages.json';
|
||||
import ptBrMessages from '../../../_locales/pt-BR/messages.json';
|
||||
import zhCnMessages from '../../../_locales/zh-CN/messages.json';
|
||||
|
||||
import * as expirationTimer from '../../util/expirationTimer';
|
||||
|
||||
|
@ -67,7 +67,7 @@ describe('expiration timer utilities', () => {
|
|||
const esI18n = setupI18n('es', esMessages);
|
||||
assert.strictEqual(format(esI18n, 120), '2 minutos');
|
||||
|
||||
const zhCnI18n = setupI18n('zh_CN', zhCnMessages);
|
||||
const zhCnI18n = setupI18n('zh-CN', zhCnMessages);
|
||||
assert.strictEqual(format(zhCnI18n, 60), '1 分钟');
|
||||
|
||||
// The underlying library supports the "pt" locale, not the "pt_BR" locale. That's
|
||||
|
@ -80,7 +80,7 @@ describe('expiration timer utilities', () => {
|
|||
|
||||
// The underlying library supports the Norwegian language, which is a macrolanguage
|
||||
// for Bokmål and Nynorsk.
|
||||
[setupI18n('nb', nbMessages), setupI18n('nn', nnMessages)].forEach(
|
||||
[setupI18n('nb', nbMessages), setupI18n('nn', nlMessages)].forEach(
|
||||
norwegianI18n => {
|
||||
assert.strictEqual(
|
||||
format(norwegianI18n, moment.duration(6, 'hours').asSeconds()),
|
||||
|
|
|
@ -92,7 +92,7 @@ describe('getFontNameByTextScript', () => {
|
|||
it('returns the correct font names (chinese simplified)', () => {
|
||||
const text = '敏捷的棕色狐狸跳过了懒狗';
|
||||
|
||||
const actual = getFontNameByTextScript(text, 0, setupI18n('zh_CN', {}));
|
||||
const actual = getFontNameByTextScript(text, 0, setupI18n('zh-CN', {}));
|
||||
const expected = '"PingFang SC Regular", SimHei, sans-serif';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
@ -100,7 +100,7 @@ describe('getFontNameByTextScript', () => {
|
|||
it('returns the correct font names (chinese traditional)', () => {
|
||||
const text = '敏捷的棕色狐狸跳過了懶狗';
|
||||
|
||||
const actual = getFontNameByTextScript(text, 0, setupI18n('zh_TW', {}));
|
||||
const actual = getFontNameByTextScript(text, 0, setupI18n('zh-TW', {}));
|
||||
const expected = '"PingFang TC Regular", "JhengHei TC Regular", sans-serif';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
|
|
@ -10,6 +10,11 @@ import { MINUTE } from '../../util/durations';
|
|||
|
||||
import type { SystemTrayServiceOptionsType } from '../../../app/SystemTrayService';
|
||||
import { SystemTrayService } from '../../../app/SystemTrayService';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
describe('SystemTrayService', function thisNeeded() {
|
||||
// These tests take more time on CI in some cases, so we increase the timeout.
|
||||
|
@ -28,12 +33,7 @@ describe('SystemTrayService', function thisNeeded() {
|
|||
options?: Partial<SystemTrayServiceOptionsType>
|
||||
): SystemTrayService {
|
||||
const result = new SystemTrayService({
|
||||
messages: {
|
||||
hide: { message: 'Hide' },
|
||||
quit: { message: 'Quit' },
|
||||
show: { message: 'Show' },
|
||||
signalDesktop: { message: 'Signal' },
|
||||
},
|
||||
i18n,
|
||||
...options,
|
||||
});
|
||||
servicesCreated.add(result);
|
||||
|
|
|
@ -196,7 +196,7 @@ const PLATFORMS = [
|
|||
];
|
||||
|
||||
describe('createTemplate', () => {
|
||||
const { messages } = loadLocale({
|
||||
const { i18n } = loadLocale({
|
||||
appLocale: 'en',
|
||||
logger: {
|
||||
error(arg: unknown) {
|
||||
|
@ -237,7 +237,7 @@ describe('createTemplate', () => {
|
|||
...actions,
|
||||
};
|
||||
|
||||
const actual = createTemplate(options, messages);
|
||||
const actual = createTemplate(options, i18n);
|
||||
assert.deepEqual(actual, expectedDefault);
|
||||
});
|
||||
|
||||
|
@ -265,7 +265,7 @@ describe('createTemplate', () => {
|
|||
return menuItem;
|
||||
});
|
||||
|
||||
const actual = createTemplate(options, messages);
|
||||
const actual = createTemplate(options, i18n);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +1,39 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import type { LocalizerType } from './Util';
|
||||
|
||||
export type LocaleMessagesType = {
|
||||
[key: string]: {
|
||||
message: string;
|
||||
description?: string;
|
||||
placeholders?: {
|
||||
[name: string]: {
|
||||
content: string;
|
||||
example: string;
|
||||
};
|
||||
export type { LocalizerType } from './Util';
|
||||
|
||||
type SmartlingConfigType = {
|
||||
placeholder_format_custom: string;
|
||||
translate_paths: Array<{
|
||||
key: string;
|
||||
path: string;
|
||||
instruction: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
type LocaleMessageType = {
|
||||
message: string;
|
||||
description?: string;
|
||||
placeholders?: {
|
||||
[name: string]: {
|
||||
content: string;
|
||||
example: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type LocaleMessagesType = {
|
||||
// In practice, 'smartling' is the only key which is a SmartlingConfigType, but
|
||||
// we get typescript error 2411 (incompatible type signatures) if we try to
|
||||
// special-case that key.
|
||||
[key: string]: LocaleMessageType | SmartlingConfigType;
|
||||
};
|
||||
|
||||
export type ReplacementValuesType<T> = {
|
||||
[key: string]: T;
|
||||
};
|
||||
|
|
|
@ -42,7 +42,7 @@ export function format(
|
|||
|
||||
// locale strings coming from electron use a dash as separator
|
||||
// but humanizeDuration uses an underscore
|
||||
const locale: string = i18n.getLocale().replace('-', '_');
|
||||
const locale: string = i18n.getLocale().replace(/-/g, '_');
|
||||
|
||||
const localeWithoutRegion: string = locale.split('_', 1)[0];
|
||||
const fallbacks: Array<string> = [];
|
||||
|
@ -56,6 +56,11 @@ export function format(
|
|||
fallbacks.push('en');
|
||||
}
|
||||
|
||||
// humanizeDuration only supports zh_CN and zh_TW
|
||||
if (locale === 'zh_HK') {
|
||||
fallbacks.push('zh_TW');
|
||||
}
|
||||
|
||||
const allUnits: Array<Unit> = ['y', 'mo', 'w', 'd', 'h', 'm', 's'];
|
||||
|
||||
const defaultUnits: Array<Unit> =
|
||||
|
|
|
@ -131,9 +131,9 @@ export function getFontNameByTextScript(
|
|||
if (fontSniffer.hasCJK(text)) {
|
||||
const locale = i18n?.getLocale();
|
||||
|
||||
if (locale === 'zh_TW') {
|
||||
if (locale === 'zh-TW') {
|
||||
fonts.push(FONT_MAP.zhtc[textStyleIndex]);
|
||||
} else if (locale === 'zh_HK') {
|
||||
} else if (locale === 'zh-HK') {
|
||||
fonts.push(FONT_MAP.zhhk[textStyleIndex]);
|
||||
} else {
|
||||
fonts.push(FONT_MAP.zhsc[textStyleIndex]);
|
||||
|
|
|
@ -38,6 +38,7 @@ const FILES_TO_IGNORE = new Set(
|
|||
[
|
||||
'.github/ISSUE_TEMPLATE/bug_report.md',
|
||||
'.github/PULL_REQUEST_TEMPLATE.md',
|
||||
'.smartling-source.sh',
|
||||
'components/mp3lameencoder/lib/Mp3LameEncoder.js',
|
||||
'components/recorderjs/recorder.js',
|
||||
'components/recorderjs/recorderWorker.js',
|
||||
|
|
|
@ -18,7 +18,7 @@ export function setupI18n(
|
|||
|
||||
const getMessage: LocalizerType = (key, substitutions) => {
|
||||
const entry = messages[key];
|
||||
if (!entry) {
|
||||
if (!entry || !('message' in entry)) {
|
||||
log.error(
|
||||
`i18n: Attempted to get translation for nonexistent key '${key}'`
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue