Move to smartling for translation services

This commit is contained in:
Scott Nonnenberg 2022-09-27 14:01:06 -07:00 committed by GitHub
parent 620067342a
commit 5957c111cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 1394 additions and 64465 deletions

1
.gitignore vendored
View File

@ -17,6 +17,7 @@ release/
/start.sh
.eslintcache
tsconfig.tsbuildinfo
.smartling-source.sh
# generated files
js/components.js

9
.smartling-source-example.sh Executable file
View 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
View 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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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 {

View File

@ -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');

View File

@ -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);

View File

@ -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'),
},
];

View File

@ -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',
});
}

View File

@ -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",

View File

@ -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}
>

View File

@ -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 (

View File

@ -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') {

View File

@ -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);
}

View 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],
}
);

View File

@ -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()),

View File

@ -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);
});

View File

@ -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);

View File

@ -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);
});
});

View File

@ -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;
};

View File

@ -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> =

View File

@ -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]);

View File

@ -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',

View File

@ -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}'`
);