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