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

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