support icu messageformat for translations
This commit is contained in:
parent
b5c514e1d1
commit
6d56f8b8aa
35 changed files with 839 additions and 104 deletions
|
@ -1,4 +1,3 @@
|
|||
build/**
|
||||
components/**
|
||||
coverage/**
|
||||
dist/**
|
||||
|
|
|
@ -191,6 +191,7 @@ module.exports = {
|
|||
'app/**/*.ts',
|
||||
'sticker-creator/**/*.ts',
|
||||
'sticker-creator/**/*.tsx',
|
||||
'build/intl-linter/**/*.ts',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
|
@ -211,7 +212,12 @@ module.exports = {
|
|||
rules: typescriptRules,
|
||||
},
|
||||
{
|
||||
files: ['**/*.stories.tsx', 'ts/build/**', 'ts/test-*/**'],
|
||||
files: [
|
||||
'**/*.stories.tsx',
|
||||
'ts/build/**',
|
||||
'ts/test-*/**',
|
||||
'build/intl-linter/**/*.ts',
|
||||
],
|
||||
rules: {
|
||||
...typescriptRules,
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
|
||||
Signal Desktop makes use of the following open source projects.
|
||||
|
||||
## @formatjs/fast-memoize
|
||||
|
||||
License: MIT
|
||||
|
||||
## @indutny/frameless-titlebar
|
||||
|
||||
MIT License
|
||||
|
@ -2863,6 +2867,10 @@ Signal Desktop makes use of the following open source projects.
|
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## react-intl
|
||||
|
||||
License: BSD-3-Clause
|
||||
|
||||
## react-measure
|
||||
|
||||
The MIT License (MIT)
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
{
|
||||
"smartling": {
|
||||
"placeholder_format_custom": "(\\$.+?\\$)",
|
||||
"string_format_paths": "icu: [*/messageformat]",
|
||||
"translate_paths": [
|
||||
{
|
||||
"path": "*/messageformat",
|
||||
"key": "{*}/messageformat",
|
||||
"instruction": "*/description"
|
||||
},
|
||||
{
|
||||
"key": "{*}/message",
|
||||
"path": "*/message",
|
||||
|
@ -733,8 +739,8 @@
|
|||
"message": "SMS/MMS contacts are not available on Desktop.",
|
||||
"description": "Shown in the search left pane when no results were found and primary device has SMS/MMS handling enabled"
|
||||
},
|
||||
"noSearchResultsInConversation": {
|
||||
"message": "No results for \"$searchTerm$\" in $conversationName$",
|
||||
"icu:noSearchResultsInConversation": {
|
||||
"messageformat": "No results for \"{searchTerm}\" in {conversationName}",
|
||||
"description": "Shown in the search left pane when no results were found"
|
||||
},
|
||||
"conversationsHeader": {
|
||||
|
@ -1212,12 +1218,8 @@
|
|||
"message": "Tap $plusButton$ (Android) or $linkNewDevice$ (iPhone)",
|
||||
"description": "Instructions on the device link screen"
|
||||
},
|
||||
"Install__qr-failed": {
|
||||
"message": "The QR code couldn't load. Check your internet and try again. $learnMore$",
|
||||
"description": "Shown on the install screen if the QR code fails to load"
|
||||
},
|
||||
"Install__qr-failed__learn-more": {
|
||||
"message": "Learn more",
|
||||
"icu:Install__qr-failed": {
|
||||
"messageformat": "The QR code couldn't load. Check your internet and try again. <learnMoreLink>Learn more</learnMoreLink>",
|
||||
"description": "Shown on the install screen if the QR code fails to load"
|
||||
},
|
||||
"Install__support-link": {
|
||||
|
@ -1777,8 +1779,8 @@
|
|||
"message": "off",
|
||||
"description": "Label for option to turn off message expiration in the timer menu"
|
||||
},
|
||||
"disappearingMessages": {
|
||||
"message": "Disappearing messages",
|
||||
"icu:disappearingMessages": {
|
||||
"messageformat": "Disappearing messages",
|
||||
"description": "Conversation menu option to enable disappearing messages. Title of the settings section for Disappearing Messages. Label of the disappearing timer select in group creation flow"
|
||||
},
|
||||
"disappearingMessagesDisabled": {
|
||||
|
@ -2741,8 +2743,8 @@
|
|||
"message": "Back",
|
||||
"description": "Default text for the previous button on all stages of the sticker creator"
|
||||
},
|
||||
"StickerCreator--DropStage--title": {
|
||||
"message": "Add your stickers",
|
||||
"icu:StickerCreator--DropStage--title": {
|
||||
"messageformat": "Add your stickers",
|
||||
"description": "Title for the drop stage of the sticker creator"
|
||||
},
|
||||
"StickerCreator--DropStage--removeSticker": {
|
||||
|
@ -2761,8 +2763,8 @@
|
|||
"message": "Show margins",
|
||||
"description": "Text for the show margins toggle on the drop stage of the sticker creator"
|
||||
},
|
||||
"StickerCreator--DropStage--addMore": {
|
||||
"message": "Add $count$ or more",
|
||||
"icu:StickerCreator--DropStage--addMore": {
|
||||
"messageformat": "Add {count, number} or more",
|
||||
"description": "Text to show user how many more stickers they must add"
|
||||
},
|
||||
"StickerCreator--EmojiStage--title": {
|
||||
|
@ -2841,8 +2843,8 @@
|
|||
"message": "Check out this new sticker pack I created for Signal. #makeprivacystick",
|
||||
"description": "Text which is shared to social media platforms for sticker packs"
|
||||
},
|
||||
"StickerCreator--Toasts--imagesAdded": {
|
||||
"message": "$count$ image(s) added",
|
||||
"icu:StickerCreator--Toasts--imagesAdded": {
|
||||
"messageformat": "{count, plural, one {1 image} other {# images}} added",
|
||||
"description": "Text for the toast when images are added to the sticker creator"
|
||||
},
|
||||
"StickerCreator--Toasts--animated": {
|
||||
|
@ -4569,8 +4571,8 @@
|
|||
"message": ".5",
|
||||
"description": "Button in the voice note message widget that shows the current playback rate of .5x (half speed) and allows the user to toggle to the next rate. Don't include the 'x'."
|
||||
},
|
||||
"emptyInboxMessage": {
|
||||
"message": "Click the $composeIcon$ above and search for your contacts or groups to message.",
|
||||
"icu:emptyInboxMessage": {
|
||||
"messageformat": "Click the {composeIcon} above and search for your contacts or groups to message.",
|
||||
"description": "Shown in the left-pane when the inbox is empty"
|
||||
},
|
||||
"composeIcon": {
|
||||
|
@ -4985,8 +4987,8 @@
|
|||
"message": "Would you like to discard these changes?",
|
||||
"description": "ConfirmationDialog text for discarding changes"
|
||||
},
|
||||
"ProfileEditor--info": {
|
||||
"message": "Your profile is encrypted. Your profile and changes to it will be visible to your contacts and when you start or accept new chats. $learnMore$",
|
||||
"icu:ProfileEditor--info": {
|
||||
"messageformat": "Your profile is encrypted. Your profile and changes to it will be visible to your contacts and when you start or accept new chats. {learnMore}",
|
||||
"description": "Information shown at the bottom of the profile editor section"
|
||||
},
|
||||
"ProfileEditor--learnMore": {
|
||||
|
|
177
build/intl-linter/linter.ts
Normal file
177
build/intl-linter/linter.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { parse as parseIcuMessage } from '@formatjs/icu-messageformat-parser';
|
||||
import type {
|
||||
MessageFormatElement,
|
||||
Location,
|
||||
} from '@formatjs/icu-messageformat-parser';
|
||||
import parseJsonToAst from 'json-to-ast';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join as pathJoin, relative as pathRelative } from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { deepEqual } from 'assert';
|
||||
import type { Rule } from './utils/rule';
|
||||
|
||||
import icuPrefix from './rules/icuPrefix';
|
||||
import onePlural from './rules/onePlural';
|
||||
import noLegacyVariables from './rules/noLegacyVariables';
|
||||
import noNestedChoice from './rules/noNestedChoice';
|
||||
import noOffset from './rules/noOffset';
|
||||
import noOrdinal from './rules/noOrdinal';
|
||||
|
||||
const RULES = [
|
||||
icuPrefix,
|
||||
noLegacyVariables,
|
||||
noNestedChoice,
|
||||
noOffset,
|
||||
noOrdinal,
|
||||
onePlural,
|
||||
];
|
||||
|
||||
type Test = {
|
||||
messageformat: string;
|
||||
expectErrors: Array<string>;
|
||||
};
|
||||
|
||||
const tests: Record<string, Test> = {
|
||||
err1: {
|
||||
messageformat: '{a, plural, other {a}} {b, plural, other {b}}',
|
||||
expectErrors: ['onePlural'],
|
||||
},
|
||||
err2: {
|
||||
messageformat: '{a, plural, other {{b, plural, other {b}}}}',
|
||||
expectErrors: ['noNestedChoice', 'onePlural'],
|
||||
},
|
||||
err3: {
|
||||
messageformat: '{a, select, other {{b, select, other {b}}}}',
|
||||
expectErrors: ['noNestedChoice'],
|
||||
},
|
||||
err4: {
|
||||
messageformat: '{a, plural, offset:1 other {a}}',
|
||||
expectErrors: ['noOffset'],
|
||||
},
|
||||
err5: {
|
||||
messageformat: '{a, selectordinal, other {a}}',
|
||||
expectErrors: ['noOrdinal'],
|
||||
},
|
||||
err6: {
|
||||
messageformat: '$a$',
|
||||
expectErrors: ['noLegacyVariables'],
|
||||
},
|
||||
};
|
||||
|
||||
type Report = {
|
||||
id: string;
|
||||
message: string;
|
||||
location: Location | void;
|
||||
};
|
||||
|
||||
function lintMessage(
|
||||
messageId: string,
|
||||
elements: Array<MessageFormatElement>,
|
||||
rules: Array<Rule>
|
||||
) {
|
||||
const reports: Array<Report> = [];
|
||||
for (const rule of rules) {
|
||||
rule.run(elements, {
|
||||
messageId,
|
||||
report(message, location) {
|
||||
reports.push({ id: rule.id, message, location });
|
||||
},
|
||||
});
|
||||
}
|
||||
return reports;
|
||||
}
|
||||
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function lintMessages() {
|
||||
const repoRoot = pathJoin(__dirname, '..', '..');
|
||||
const filePath = pathJoin(repoRoot, '_locales/en/messages.json');
|
||||
const relativePath = pathRelative(repoRoot, filePath);
|
||||
let file = await readFile(filePath, 'utf-8');
|
||||
|
||||
if (process.argv.includes('--test')) {
|
||||
file = JSON.stringify(tests);
|
||||
}
|
||||
|
||||
const jsonAst = parseJsonToAst(file);
|
||||
|
||||
assert(jsonAst.type === 'Object', 'Expected an object');
|
||||
for (const topProp of jsonAst.children) {
|
||||
if (topProp.key.value === 'smartling') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const messageId = topProp.key.value;
|
||||
|
||||
assert(topProp.value.type === 'Object', 'Expected an object');
|
||||
|
||||
const icuMessageProp = topProp.value.children.find(messageProp => {
|
||||
return messageProp.key.value === 'messageformat';
|
||||
});
|
||||
if (icuMessageProp == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const icuMesssageLiteral = icuMessageProp.value;
|
||||
assert(
|
||||
icuMesssageLiteral.type === 'Literal' &&
|
||||
typeof icuMesssageLiteral.value === 'string',
|
||||
'Expected a string'
|
||||
);
|
||||
|
||||
const icuMessage: string = icuMesssageLiteral.value;
|
||||
|
||||
const ast = parseIcuMessage(icuMessage, {
|
||||
captureLocation: true,
|
||||
shouldParseSkeletons: true,
|
||||
requiresOtherClause: true,
|
||||
});
|
||||
|
||||
const reports = lintMessage(messageId, ast, RULES);
|
||||
const key = topProp.key.value;
|
||||
|
||||
if (process.argv.includes('--test')) {
|
||||
const test = tests[key];
|
||||
const actualErrors = reports.map(report => report.id);
|
||||
deepEqual(actualErrors, test.expectErrors);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const report of reports) {
|
||||
let loc = '';
|
||||
|
||||
if (report.location != null && icuMesssageLiteral.loc != null) {
|
||||
const line =
|
||||
icuMesssageLiteral.loc.start.line + (report.location.start.line - 1);
|
||||
const column =
|
||||
icuMesssageLiteral.loc.start.column + report.location.start.column;
|
||||
loc = `:${line}:${column}`;
|
||||
} else if (icuMesssageLiteral.loc != null) {
|
||||
const { line, column } = icuMesssageLiteral.loc.start;
|
||||
loc = `:${line}:${column}`;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
chalk`{bold.cyan ${relativePath}${loc}} ${report.message} {magenta ({underline ${report.id}})}`
|
||||
);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk` {dim in ${key} is "}{red ${icuMessage}}{dim "}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lintMessages().catch(error => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
11
build/intl-linter/rules/icuPrefix.ts
Normal file
11
build/intl-linter/rules/icuPrefix.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { rule } from '../utils/rule';
|
||||
|
||||
export default rule('icuPrefix', context => {
|
||||
if (!context.messageId.startsWith('icu:')) {
|
||||
context.report('ICU message IDs must start with "icu:"');
|
||||
}
|
||||
return {};
|
||||
});
|
17
build/intl-linter/rules/noLegacyVariables.ts
Normal file
17
build/intl-linter/rules/noLegacyVariables.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { rule } from '../utils/rule';
|
||||
|
||||
export default rule('noLegacyVariables', context => {
|
||||
return {
|
||||
enterLiteral(element) {
|
||||
if (/(\$.+?\$)/.test(element.value)) {
|
||||
context.report(
|
||||
'String must not contain legacy $variables$',
|
||||
element.location
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
35
build/intl-linter/rules/noNestedChoice.ts
Normal file
35
build/intl-linter/rules/noNestedChoice.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Element } from '../utils/rule';
|
||||
import { rule } from '../utils/rule';
|
||||
|
||||
export default rule('noNestedChoice', context => {
|
||||
let insideChoice = false;
|
||||
|
||||
function check(element: Element) {
|
||||
if (insideChoice) {
|
||||
context.report(
|
||||
'Nested {select}/{plural} is not supported by Smartling',
|
||||
element.location
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
enterSelect(element) {
|
||||
check(element);
|
||||
insideChoice = true;
|
||||
},
|
||||
exitSelect() {
|
||||
insideChoice = false;
|
||||
},
|
||||
enterPlural(element) {
|
||||
check(element);
|
||||
insideChoice = true;
|
||||
},
|
||||
exitPlural() {
|
||||
insideChoice = false;
|
||||
},
|
||||
};
|
||||
});
|
17
build/intl-linter/rules/noOffset.ts
Normal file
17
build/intl-linter/rules/noOffset.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { rule } from '../utils/rule';
|
||||
|
||||
export default rule('noOffset', context => {
|
||||
return {
|
||||
enterPlural(element) {
|
||||
if (element.offset !== 0) {
|
||||
context.report(
|
||||
'{plural} with offset is not supported by Smartling',
|
||||
element.location
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
17
build/intl-linter/rules/noOrdinal.ts
Normal file
17
build/intl-linter/rules/noOrdinal.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { rule } from '../utils/rule';
|
||||
|
||||
export default rule('noOrdinal', context => {
|
||||
return {
|
||||
enterPlural(element) {
|
||||
if (element.pluralType === 'ordinal') {
|
||||
context.report(
|
||||
'{selectordinal} is not supported by Smartling',
|
||||
element.location
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
19
build/intl-linter/rules/onePlural.ts
Normal file
19
build/intl-linter/rules/onePlural.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { rule } from '../utils/rule';
|
||||
|
||||
export default rule('onePlural', context => {
|
||||
let plurals = 0;
|
||||
return {
|
||||
enterPlural(element) {
|
||||
plurals += 1;
|
||||
if (plurals > 1) {
|
||||
context.report(
|
||||
'Multiple {plural} is not supported by Smartling',
|
||||
element.location
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
33
build/intl-linter/utils/rule.ts
Normal file
33
build/intl-linter/utils/rule.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { MessageFormatElement } from '@formatjs/icu-messageformat-parser';
|
||||
import { Location } from '@formatjs/icu-messageformat-parser';
|
||||
import type { Visitor } from './traverse';
|
||||
import { traverse } from './traverse';
|
||||
|
||||
export type Element = MessageFormatElement;
|
||||
export { Location };
|
||||
|
||||
export type Context = {
|
||||
messageId: string;
|
||||
report(message: string, location: Location | void): void;
|
||||
};
|
||||
|
||||
export type RuleFactory = {
|
||||
(context: Context): Visitor;
|
||||
};
|
||||
|
||||
export type Rule = {
|
||||
id: string;
|
||||
run(elements: Array<MessageFormatElement>, context: Context): void;
|
||||
};
|
||||
|
||||
export function rule(id: string, ruleFactory: RuleFactory): Rule {
|
||||
return {
|
||||
id,
|
||||
run(elements, context) {
|
||||
traverse(elements, ruleFactory(context));
|
||||
},
|
||||
};
|
||||
}
|
90
build/intl-linter/utils/traverse.ts
Normal file
90
build/intl-linter/utils/traverse.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type {
|
||||
MessageFormatElement,
|
||||
LiteralElement,
|
||||
ArgumentElement,
|
||||
NumberElement,
|
||||
DateElement,
|
||||
TimeElement,
|
||||
SelectElement,
|
||||
PluralElement,
|
||||
PoundElement,
|
||||
TagElement,
|
||||
} from '@formatjs/icu-messageformat-parser';
|
||||
import { TYPE } from '@formatjs/icu-messageformat-parser';
|
||||
|
||||
export type VisitorMethod<T extends MessageFormatElement> = (
|
||||
element: T
|
||||
) => void;
|
||||
|
||||
export type Visitor = {
|
||||
enterLiteral?: VisitorMethod<LiteralElement>;
|
||||
exitLiteral?: VisitorMethod<LiteralElement>;
|
||||
enterArgument?: VisitorMethod<ArgumentElement>;
|
||||
exitArgument?: VisitorMethod<ArgumentElement>;
|
||||
enterNumber?: VisitorMethod<NumberElement>;
|
||||
exitNumber?: VisitorMethod<NumberElement>;
|
||||
enterDate?: VisitorMethod<DateElement>;
|
||||
exitDate?: VisitorMethod<DateElement>;
|
||||
enterTime?: VisitorMethod<TimeElement>;
|
||||
exitTime?: VisitorMethod<TimeElement>;
|
||||
enterSelect?: VisitorMethod<SelectElement>;
|
||||
exitSelect?: VisitorMethod<SelectElement>;
|
||||
enterPlural?: VisitorMethod<PluralElement>;
|
||||
exitPlural?: VisitorMethod<PluralElement>;
|
||||
enterPound?: VisitorMethod<PoundElement>;
|
||||
exitPound?: VisitorMethod<PoundElement>;
|
||||
enterTag?: VisitorMethod<TagElement>;
|
||||
exitTag?: VisitorMethod<TagElement>;
|
||||
};
|
||||
|
||||
export function traverse(
|
||||
elements: Array<MessageFormatElement>,
|
||||
visitor: Visitor
|
||||
): void {
|
||||
for (const element of elements) {
|
||||
if (element.type === TYPE.literal) {
|
||||
visitor.enterLiteral?.(element);
|
||||
visitor.exitLiteral?.(element);
|
||||
} else if (element.type === TYPE.argument) {
|
||||
visitor.enterArgument?.(element);
|
||||
visitor.exitArgument?.(element);
|
||||
} else if (element.type === TYPE.number) {
|
||||
visitor.enterNumber?.(element);
|
||||
visitor.exitNumber?.(element);
|
||||
} else if (element.type === TYPE.date) {
|
||||
visitor.enterDate?.(element);
|
||||
visitor.exitDate?.(element);
|
||||
} else if (element.type === TYPE.time) {
|
||||
visitor.enterTime?.(element);
|
||||
visitor.exitTime?.(element);
|
||||
} else if (element.type === TYPE.select) {
|
||||
visitor.enterSelect?.(element);
|
||||
for (const node of Object.values(element.options)) {
|
||||
traverse(node.value, visitor);
|
||||
}
|
||||
visitor.exitSelect?.(element);
|
||||
} else if (element.type === TYPE.plural) {
|
||||
visitor.enterPlural?.(element);
|
||||
for (const node of Object.values(element.options)) {
|
||||
traverse(node.value, visitor);
|
||||
}
|
||||
visitor.exitPlural?.(element);
|
||||
} else if (element.type === TYPE.pound) {
|
||||
visitor.enterPound?.(element);
|
||||
visitor.exitPound?.(element);
|
||||
} else if (element.type === TYPE.tag) {
|
||||
visitor.enterTag?.(element);
|
||||
traverse(element.children, visitor);
|
||||
visitor.exitTag?.(element);
|
||||
} else {
|
||||
unreachable(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unreachable(x: never): never {
|
||||
throw new Error(`unreachable: ${x}`);
|
||||
}
|
11
package.json
11
package.json
|
@ -34,17 +34,19 @@
|
|||
"prepare-staging-build": "node scripts/prepare_staging_build.js",
|
||||
"prepare-windows-cert": "node scripts/prepare_windows_cert.js",
|
||||
"publish-to-apt": "NAME=$npm_package_name VERSION=$npm_package_version ./aptly.sh",
|
||||
"test": "yarn test-node && yarn test-electron",
|
||||
"test": "yarn test-node && yarn test-electron && yarn test-lint-intl",
|
||||
"test-electron": "node ts/scripts/test-electron.js",
|
||||
"test-release": "node ts/scripts/test-release.js",
|
||||
"test-node": "electron-mocha --timeout 10000 --file test/setup-test-node.js --recursive test/modules ts/test-node ts/test-both",
|
||||
"test-mock": "mocha ts/test-mock/**/*_test.js",
|
||||
"test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/modules ts/test-node ts/test-both",
|
||||
"test-lint-intl": "ts-node ./build/intl-linter/linter.ts --test",
|
||||
"eslint": "eslint --cache .",
|
||||
"lint": "run-s --print-label lint-prettier check:types eslint",
|
||||
"lint-deps": "node ts/util/lint/linter.js",
|
||||
"lint-license-comments": "ts-node ts/util/lint/license_comments.ts",
|
||||
"lint-prettier": "pprettier --check '**/*.{ts,tsx,d.ts,js,json,html,scss,md,yml,yaml}' '!node_modules/**'",
|
||||
"lint-intl": "ts-node ./build/intl-linter/linter.ts",
|
||||
"danger:local": "./danger/danger.sh local --base main",
|
||||
"danger:ci": "./danger/danger.sh ci --base origin/main",
|
||||
"format": "pprettier --write '**/*.{ts,tsx,d.ts,js,json,html,scss,md,yml,yaml}' '!node_modules/**'",
|
||||
|
@ -53,7 +55,7 @@
|
|||
"clean-transpile-once": "rimraf app/**/*.js app/*.js sticker-creator/**/*.js sticker-creator/*.js ts/**/*.js ts/*.js tsconfig.tsbuildinfo",
|
||||
"clean-transpile": "yarn run clean-transpile-once && yarn run clean-transpile-once",
|
||||
"open-coverage": "open coverage/lcov-report/index.html",
|
||||
"ready": "npm-run-all --print-label clean-transpile generate --parallel lint lint-deps test-node test-electron",
|
||||
"ready": "npm-run-all --print-label clean-transpile generate --parallel lint lint-deps lint-intl test-node test-electron",
|
||||
"dev": "run-p --print-label dev:*",
|
||||
"dev:transpile": "run-p \"check:types --watch\" dev:esbuild",
|
||||
"dev:webpack": "run-p dev:esbuild dev:webpack:sticker-creator",
|
||||
|
@ -80,6 +82,7 @@
|
|||
"fs-xattr": "0.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/fast-memoize": "1.2.6",
|
||||
"@indutny/frameless-titlebar": "2.3.5",
|
||||
"@popperjs/core": "2.9.2",
|
||||
"@react-spring/web": "9.4.5",
|
||||
|
@ -154,6 +157,7 @@
|
|||
"react-dom": "17.0.2",
|
||||
"react-dropzone": "10.2.2",
|
||||
"react-hot-loader": "4.13.0",
|
||||
"react-intl": "6.1.1",
|
||||
"react-measure": "2.3.0",
|
||||
"react-popper": "2.3.0",
|
||||
"react-quill": "2.0.0-beta.4",
|
||||
|
@ -226,6 +230,7 @@
|
|||
"@types/intl-tel-input": "17.0.4",
|
||||
"@types/jquery": "3.5.6",
|
||||
"@types/js-yaml": "3.12.0",
|
||||
"@types/json-to-ast": "2.1.2",
|
||||
"@types/linkify-it": "2.1.0",
|
||||
"@types/lodash": "4.14.106",
|
||||
"@types/long": "4.0.1",
|
||||
|
@ -271,6 +276,7 @@
|
|||
"casual": "1.6.2",
|
||||
"chai": "4.3.4",
|
||||
"chai-as-promised": "7.1.1",
|
||||
"chalk": "4.1.2",
|
||||
"core-js": "2.6.9",
|
||||
"cross-env": "5.2.0",
|
||||
"css-loader": "3.2.0",
|
||||
|
@ -291,6 +297,7 @@
|
|||
"eslint-plugin-react": "7.20.6",
|
||||
"file-loader": "4.2.0",
|
||||
"html-webpack-plugin": "5.3.1",
|
||||
"json-to-ast": "2.1.0",
|
||||
"mocha": "9.1.3",
|
||||
"mocha-testcheck": "1.0.0-rc.0",
|
||||
"node-gyp": "9.0.0",
|
||||
|
|
|
@ -75,9 +75,9 @@ export const AppStage: React.ComponentType<Props> = props => {
|
|||
) : null}
|
||||
{addMoreCount > 0 ? (
|
||||
<Text secondary>
|
||||
{i18n('StickerCreator--DropStage--addMore', [
|
||||
addMoreCount.toString(),
|
||||
])}
|
||||
{i18n('icu:StickerCreator--DropStage--addMore', {
|
||||
count: addMoreCount,
|
||||
})}
|
||||
</Text>
|
||||
) : null}
|
||||
{next || onNext ? (
|
||||
|
|
|
@ -24,7 +24,7 @@ export const DropStage: React.ComponentType = () => {
|
|||
|
||||
return (
|
||||
<AppStage next="/add-emojis" nextActive={stickersReady}>
|
||||
<H2>{i18n('StickerCreator--DropStage--title')}</H2>
|
||||
<H2>{i18n('icu:StickerCreator--DropStage--title')}</H2>
|
||||
<div className={styles.info}>
|
||||
<Text className={styles.message}>
|
||||
{i18n('StickerCreator--DropStage--help')}
|
||||
|
|
|
@ -52,7 +52,7 @@ export const UploadStage: React.ComponentType = () => {
|
|||
);
|
||||
actions.addToast({
|
||||
key: 'StickerCreator--Toasts--errorUploading',
|
||||
subs: [e.message],
|
||||
subs: { message: e.message },
|
||||
});
|
||||
history.push('/add-meta');
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ export const setPackMeta = createAction<PackMetaData>('stickers/setPackMeta');
|
|||
|
||||
export const addToast = createAction<{
|
||||
key: string;
|
||||
subs?: Array<string>;
|
||||
subs?: Record<string, string>;
|
||||
}>('stickers/addToast');
|
||||
export const dismissToast = createAction<void>('stickers/dismissToast');
|
||||
|
||||
|
@ -67,7 +67,7 @@ type StateStickerData = {
|
|||
|
||||
type StateToastData = {
|
||||
key: string;
|
||||
subs?: Array<string>;
|
||||
subs?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type State = {
|
||||
|
@ -150,7 +150,7 @@ export const reducer = reduceReducers<State>(
|
|||
if (data && !data.imageData) {
|
||||
data.imageData = payload;
|
||||
|
||||
const key = 'StickerCreator--Toasts--imagesAdded';
|
||||
const key = 'icu:StickerCreator--Toasts--imagesAdded';
|
||||
|
||||
const toast = (() => {
|
||||
const oldToast = find(state.toasts, { key });
|
||||
|
@ -159,21 +159,21 @@ export const reducer = reduceReducers<State>(
|
|||
return oldToast;
|
||||
}
|
||||
|
||||
const newToast = { key, subs: ['0'] };
|
||||
const newToast = { key, subs: { count: '0' } };
|
||||
state.toasts.push(newToast);
|
||||
|
||||
return newToast;
|
||||
})();
|
||||
|
||||
const previousSub = toast?.subs?.[0];
|
||||
const previousSub = toast?.subs?.count;
|
||||
if (toast && isString(previousSub)) {
|
||||
const previousCount = parseInt(previousSub, 10);
|
||||
const newCount = Number.isFinite(previousCount)
|
||||
? previousCount + 1
|
||||
: 1;
|
||||
|
||||
toast.subs = toast.subs || [];
|
||||
toast.subs[0] = newCount.toString();
|
||||
toast.subs = toast.subs || {};
|
||||
toast.subs.count = newCount.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,17 +2,29 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import type { LocaleMessagesType } from '../../ts/types/I18N';
|
||||
import type { LocalizerType, ReplacementValuesType } from '../../ts/types/Util';
|
||||
import {
|
||||
classifyMessages,
|
||||
createCachedIntl,
|
||||
formatIcuMessage,
|
||||
} from '../../ts/util/setupI18n';
|
||||
|
||||
const placeholder = () => 'NO LOCALE LOADED';
|
||||
placeholder.getLocale = () => 'none';
|
||||
placeholder.isLegacyFormat = () => {
|
||||
throw new Error("Can't call isLegacyFormat on placeholder");
|
||||
};
|
||||
placeholder.getIntl = () => {
|
||||
throw new Error("Can't call getIntl on placeholder");
|
||||
};
|
||||
|
||||
const I18nContext = React.createContext<LocalizerType>(placeholder);
|
||||
|
||||
export type I18nProps = {
|
||||
children: React.ReactNode;
|
||||
locale: string;
|
||||
messages: { [key: string]: { message: string } };
|
||||
messages: LocaleMessagesType;
|
||||
};
|
||||
|
||||
export const I18n = ({
|
||||
|
@ -20,6 +32,13 @@ export const I18n = ({
|
|||
locale,
|
||||
children,
|
||||
}: I18nProps): JSX.Element => {
|
||||
const { icuMessages, legacyMessages } = React.useMemo(() => {
|
||||
return classifyMessages(messages);
|
||||
}, [messages]);
|
||||
const intl = React.useMemo(() => {
|
||||
return createCachedIntl(locale, icuMessages);
|
||||
}, [locale, icuMessages]);
|
||||
|
||||
const callback = (key: string, substitutions?: ReplacementValuesType) => {
|
||||
if (Array.isArray(substitutions) && substitutions.length > 1) {
|
||||
throw new Error(
|
||||
|
@ -27,15 +46,18 @@ export const I18n = ({
|
|||
);
|
||||
}
|
||||
|
||||
const stringInfo = messages[key];
|
||||
if (!stringInfo) {
|
||||
const messageformat = icuMessages[key];
|
||||
if (messageformat != null) {
|
||||
return formatIcuMessage(intl, key, substitutions);
|
||||
}
|
||||
|
||||
const message = legacyMessages[key];
|
||||
if (message == null) {
|
||||
window.SignalContext.log.warn(
|
||||
`getMessage: No string found for key ${key}`
|
||||
);
|
||||
return '';
|
||||
}
|
||||
|
||||
const { message } = stringInfo;
|
||||
if (!substitutions) {
|
||||
return message;
|
||||
}
|
||||
|
@ -79,8 +101,16 @@ export const I18n = ({
|
|||
return builder;
|
||||
};
|
||||
callback.getLocale = () => locale;
|
||||
callback.isLegacyFormat = (key: string) => {
|
||||
return legacyMessages[key] != null;
|
||||
};
|
||||
callback.getIntl = () => intl;
|
||||
|
||||
const getMessage = React.useCallback<LocalizerType>(callback, [messages]);
|
||||
const getMessage = React.useCallback<LocalizerType>(callback, [
|
||||
icuMessages,
|
||||
legacyMessages,
|
||||
intl,
|
||||
]);
|
||||
|
||||
return (
|
||||
<I18nContext.Provider value={getMessage}>{children}</I18nContext.Provider>
|
||||
|
|
|
@ -3,11 +3,17 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import type { FormatXMLElementFn } from 'intl-messageformat';
|
||||
import type { LocalizerType, RenderTextCallbackType } from '../types/Util';
|
||||
import type { ReplacementValuesType } from '../types/I18N';
|
||||
import * as log from '../logging/log';
|
||||
import { strictAssert } from '../util/assert';
|
||||
|
||||
export type FullJSXType = Array<JSX.Element | string> | JSX.Element | string;
|
||||
export type FullJSXType =
|
||||
| FormatXMLElementFn<JSX.Element | string>
|
||||
| Array<JSX.Element | string>
|
||||
| JSX.Element
|
||||
| string;
|
||||
export type IntlComponentsType =
|
||||
| undefined
|
||||
| Array<FullJSXType>
|
||||
|
@ -32,7 +38,7 @@ export class Intl extends React.Component<Props> {
|
|||
index: number,
|
||||
placeholderName: string,
|
||||
key: number
|
||||
): FullJSXType | null {
|
||||
): JSX.Element | null {
|
||||
const { id, components } = this.props;
|
||||
|
||||
if (!components) {
|
||||
|
@ -75,6 +81,15 @@ export class Intl extends React.Component<Props> {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!i18n.isLegacyFormat(id)) {
|
||||
strictAssert(
|
||||
!Array.isArray(components),
|
||||
`components cannot be an array for ICU message ${id}`
|
||||
);
|
||||
const intl = i18n.getIntl();
|
||||
return intl.formatMessage({ id }, components);
|
||||
}
|
||||
|
||||
const text = i18n(id);
|
||||
const results: Array<
|
||||
string | JSX.Element | Array<string | JSX.Element> | null
|
||||
|
|
|
@ -915,7 +915,7 @@ export const Preferences = ({
|
|||
onSubmit={onUniversalExpireTimerChange}
|
||||
/>
|
||||
)}
|
||||
<SettingsRow title={i18n('disappearingMessages')}>
|
||||
<SettingsRow title={i18n('icu:disappearingMessages')}>
|
||||
<Control
|
||||
left={
|
||||
<>
|
||||
|
|
|
@ -798,7 +798,7 @@ export const ProfileEditor = ({
|
|||
<div className="ProfileEditor__info">
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="ProfileEditor--info"
|
||||
id="icu:ProfileEditor--info"
|
||||
components={{
|
||||
learnMore: (
|
||||
<a
|
||||
|
|
|
@ -347,7 +347,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
const muteOptions = getMuteOptions(muteExpiresAt, i18n);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const disappearingTitle = i18n('disappearingMessages') as any;
|
||||
const disappearingTitle = i18n('icu:disappearingMessages') as any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const muteTitle = i18n('muteNotificationsTitle') as any;
|
||||
const isGroup = type === 'group';
|
||||
|
|
|
@ -98,12 +98,12 @@ function InstallScreenQrCode(
|
|||
<span className={classNames(getQrCodeClassName('__error-message'))}>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="Install__qr-failed"
|
||||
components={[
|
||||
<a href={QR_CODE_FAILED_LINK}>
|
||||
{i18n('Install__qr-failed__learn-more')}
|
||||
</a>,
|
||||
]}
|
||||
id="icu:Install__qr-failed"
|
||||
components={{
|
||||
learnMoreLink: children => (
|
||||
<a href={QR_CODE_FAILED_LINK}>{children}</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -120,15 +120,17 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
|
|||
<div>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="emptyInboxMessage"
|
||||
components={[
|
||||
id="icu:emptyInboxMessage"
|
||||
components={{
|
||||
composeIcon: (
|
||||
<span>
|
||||
<strong>{i18n('composeIcon')}</strong>
|
||||
<span className="module-left-pane__empty--composer_icon">
|
||||
<i className="module-left-pane__empty--composer_icon--icon" />
|
||||
</span>
|
||||
</span>,
|
||||
]}
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -148,7 +148,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
|||
if (searchConversationName) {
|
||||
noResults = (
|
||||
<Intl
|
||||
id="noSearchResultsInConversation"
|
||||
id="icu:noSearchResultsInConversation"
|
||||
i18n={i18n}
|
||||
components={{
|
||||
searchTerm,
|
||||
|
|
|
@ -199,7 +199,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
|
|||
|
||||
<section className="module-left-pane__header__form__expire-timer">
|
||||
<div className="module-left-pane__header__form__expire-timer__label">
|
||||
{i18n('disappearingMessages')}
|
||||
{i18n('icu:disappearingMessages')}
|
||||
</div>
|
||||
<DisappearingTimerSelect
|
||||
i18n={i18n}
|
||||
|
|
|
@ -89,6 +89,10 @@ function manualReconnect(): NoopActionType {
|
|||
};
|
||||
}
|
||||
|
||||
const intlNotSetup = () => {
|
||||
throw new Error('i18n not yet set up');
|
||||
};
|
||||
|
||||
// Reducer
|
||||
|
||||
export function getEmptyState(): UserStateType {
|
||||
|
@ -114,16 +118,11 @@ export function getEmptyState(): UserStateType {
|
|||
platform: 'unknown',
|
||||
},
|
||||
theme: ThemeType.light,
|
||||
i18n: Object.assign(
|
||||
() => {
|
||||
throw new Error('i18n not yet set up');
|
||||
},
|
||||
{
|
||||
getLocale() {
|
||||
throw new Error('i18n not yet set up');
|
||||
},
|
||||
}
|
||||
),
|
||||
i18n: Object.assign(intlNotSetup, {
|
||||
getLocale: intlNotSetup,
|
||||
getIntl: intlNotSetup,
|
||||
isLegacyFormat: intlNotSetup,
|
||||
}),
|
||||
localeMessages: {},
|
||||
version: '0.0.0',
|
||||
};
|
||||
|
|
|
@ -34,6 +34,15 @@ describe('setupI18n', () => {
|
|||
'Someone set the disappearing message time to 5 minutes.'
|
||||
);
|
||||
});
|
||||
it('returns a modern icu message formatted', () => {
|
||||
const actual = i18n('icu:ProfileEditor--info', {
|
||||
learnMore: 'LEARN MORE',
|
||||
});
|
||||
assert.equal(
|
||||
actual,
|
||||
'Your profile is encrypted. Your profile and changes to it will be visible to your contacts and when you start or accept new chats. LEARN MORE'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocale', () => {
|
||||
|
@ -42,4 +51,26 @@ describe('setupI18n', () => {
|
|||
assert.isAtLeast(locale.trim().length, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIntl', () => {
|
||||
it('returns the intl object to call formatMessage()', () => {
|
||||
const intl = i18n.getIntl();
|
||||
assert.isObject(intl);
|
||||
const result = intl.formatMessage(
|
||||
{ id: 'icu:emptyInboxMessage' },
|
||||
{ composeIcon: 'ICONIC' }
|
||||
);
|
||||
assert.equal(
|
||||
result,
|
||||
'Click the ICONIC above and search for your contacts or groups to message.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLegacyFormat', () => {
|
||||
it('returns false for new format', () => {
|
||||
assert.isFalse(i18n.isLegacyFormat('icu:ProfileEditor--info'));
|
||||
assert.isTrue(i18n.isLegacyFormat('softwareAcknowledgments'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -67,7 +67,11 @@ describe('MentionCompletion', () => {
|
|||
|
||||
const options: MentionCompletionOptions = {
|
||||
getPreferredBadge: () => undefined,
|
||||
i18n: Object.assign(sinon.stub(), { getLocale: sinon.stub() }),
|
||||
i18n: Object.assign(sinon.stub(), {
|
||||
getLocale: sinon.stub(),
|
||||
getIntl: sinon.stub(),
|
||||
isLegacyFormat: sinon.stub(),
|
||||
}),
|
||||
me,
|
||||
memberRepositoryRef,
|
||||
setMentionPickerElement: sinon.stub(),
|
||||
|
|
|
@ -9,6 +9,7 @@ export type { LocalizerType } from './Util';
|
|||
|
||||
type SmartlingConfigType = {
|
||||
placeholder_format_custom: string;
|
||||
string_format_paths?: string;
|
||||
translate_paths: Array<{
|
||||
key: string;
|
||||
path: string;
|
||||
|
@ -16,15 +17,10 @@ type SmartlingConfigType = {
|
|||
}>;
|
||||
};
|
||||
|
||||
type LocaleMessageType = {
|
||||
message: string;
|
||||
export type LocaleMessageType = {
|
||||
message?: string;
|
||||
messageformat?: string;
|
||||
description?: string;
|
||||
placeholders?: {
|
||||
[name: string]: {
|
||||
content: string;
|
||||
example: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type LocaleMessagesType = {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { IntlShape } from 'react-intl';
|
||||
import type { UUIDStringType } from './UUID';
|
||||
|
||||
export type BodyRangeType = {
|
||||
|
@ -31,6 +32,8 @@ export type ReplacementValuesType =
|
|||
|
||||
export type LocalizerType = {
|
||||
(key: string, values?: ReplacementValuesType): string;
|
||||
getIntl(): IntlShape;
|
||||
isLegacyFormat(key: string): boolean;
|
||||
getLocale(): string;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,9 +1,87 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { LocaleMessagesType } from '../types/I18N';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import memoize from '@formatjs/fast-memoize';
|
||||
import type { IntlShape } from 'react-intl';
|
||||
import { createIntl, createIntlCache } from 'react-intl';
|
||||
import type { LocaleMessageType, LocaleMessagesType } from '../types/I18N';
|
||||
import type { LocalizerType, ReplacementValuesType } from '../types/Util';
|
||||
import * as log from '../logging/log';
|
||||
import { strictAssert } from './assert';
|
||||
|
||||
export const formatters = {
|
||||
getNumberFormat: memoize((locale, opts) => {
|
||||
return new Intl.NumberFormat(locale, opts);
|
||||
}),
|
||||
getDateTimeFormat: memoize((locale, opts) => {
|
||||
return new Intl.DateTimeFormat(locale, opts);
|
||||
}),
|
||||
getPluralRules: memoize((locale, opts) => {
|
||||
return new Intl.PluralRules(locale, opts);
|
||||
}),
|
||||
};
|
||||
|
||||
export function isLocaleMessageType(
|
||||
value: unknown
|
||||
): value is LocaleMessageType {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value != null &&
|
||||
(Object.hasOwn(value, 'message') || Object.hasOwn(value, 'messageformat'))
|
||||
);
|
||||
}
|
||||
|
||||
export function classifyMessages(messages: LocaleMessagesType): {
|
||||
icuMessages: Record<string, string>;
|
||||
legacyMessages: Record<string, string>;
|
||||
} {
|
||||
const icuMessages: Record<string, string> = {};
|
||||
const legacyMessages: Record<string, string> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(messages)) {
|
||||
if (isLocaleMessageType(value)) {
|
||||
if (value.messageformat != null) {
|
||||
icuMessages[key] = value.messageformat;
|
||||
} else if (value.message != null) {
|
||||
legacyMessages[key] = value.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { icuMessages, legacyMessages };
|
||||
}
|
||||
|
||||
export function createCachedIntl(
|
||||
locale: string,
|
||||
icuMessages: Record<string, string>
|
||||
): IntlShape {
|
||||
const intlCache = createIntlCache();
|
||||
const intl = createIntl(
|
||||
{
|
||||
locale: locale.replace('_', '-'), // normalize supported locales to browser format
|
||||
messages: icuMessages,
|
||||
},
|
||||
intlCache
|
||||
);
|
||||
return intl;
|
||||
}
|
||||
|
||||
export function formatIcuMessage(
|
||||
intl: IntlShape,
|
||||
id: string,
|
||||
substitutions: ReplacementValuesType | undefined
|
||||
): string {
|
||||
strictAssert(
|
||||
!Array.isArray(substitutions),
|
||||
`substitutions must be an object for ICU message ${id}`
|
||||
);
|
||||
const result = intl.formatMessage({ id }, substitutions);
|
||||
strictAssert(
|
||||
typeof result === 'string',
|
||||
'i18n: formatted translation result must be a string, must use <Intl/> component to render JSX'
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function setupI18n(
|
||||
locale: string,
|
||||
|
@ -16,14 +94,24 @@ export function setupI18n(
|
|||
throw new Error('i18n: messages parameter is required');
|
||||
}
|
||||
|
||||
const { icuMessages, legacyMessages } = classifyMessages(messages);
|
||||
const intl = createCachedIntl(locale, icuMessages);
|
||||
|
||||
const getMessage: LocalizerType = (key, substitutions) => {
|
||||
const entry = messages[key];
|
||||
if (!entry || !('message' in entry)) {
|
||||
const messageformat = icuMessages[key];
|
||||
|
||||
if (messageformat != null) {
|
||||
return formatIcuMessage(intl, key, substitutions);
|
||||
}
|
||||
|
||||
const message = legacyMessages[key];
|
||||
if (message == null) {
|
||||
log.error(
|
||||
`i18n: Attempted to get translation for nonexistent key '${key}'`
|
||||
);
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Array.isArray(substitutions) && substitutions.length > 1) {
|
||||
throw new Error(
|
||||
'Array syntax is not supported with more than one placeholder'
|
||||
|
@ -35,8 +123,6 @@ export function setupI18n(
|
|||
) {
|
||||
throw new Error('You must provide either a map or an array');
|
||||
}
|
||||
|
||||
const { message } = entry;
|
||||
if (!substitutions) {
|
||||
return message;
|
||||
}
|
||||
|
@ -78,6 +164,12 @@ export function setupI18n(
|
|||
return builder;
|
||||
};
|
||||
|
||||
getMessage.getIntl = () => {
|
||||
return intl;
|
||||
};
|
||||
getMessage.isLegacyFormat = (key: string) => {
|
||||
return legacyMessages[key] != null;
|
||||
};
|
||||
getMessage.getLocale = () => locale;
|
||||
|
||||
return getMessage;
|
||||
|
|
|
@ -61,5 +61,11 @@
|
|||
// "experimentalDecorators": true, // Enables experimental support for ES7 decorators.
|
||||
// "emitDecoratorMetadata": true, // Enables experimental support for emitting type metadata for decorators.
|
||||
},
|
||||
"include": ["ts/**/*", "app/**/*", "sticker-creator/**/*", "package.json"]
|
||||
"include": [
|
||||
"ts/**/*",
|
||||
"app/**/*",
|
||||
"sticker-creator/**/*",
|
||||
"package.json",
|
||||
"build/intl-linter/**/*"
|
||||
]
|
||||
}
|
||||
|
|
149
yarn.lock
149
yarn.lock
|
@ -1453,6 +1453,14 @@
|
|||
"@formatjs/intl-localematcher" "0.2.25"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/ecma402-abstract@1.12.0":
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.12.0.tgz#2fb5e8983d5fae2fad9ec6c77aec1803c2b88d8e"
|
||||
integrity sha512-0/wm9b7brUD40kx7KSE0S532T8EfH06Zc41rGlinoNyYXnuusR6ull2x63iFJgVXgwahm42hAW7dcYdZ+llZzA==
|
||||
dependencies:
|
||||
"@formatjs/intl-localematcher" "0.2.31"
|
||||
tslib "2.4.0"
|
||||
|
||||
"@formatjs/fast-memoize@1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz#e6f5aee2e4fd0ca5edba6eba7668e2d855e0fc21"
|
||||
|
@ -1460,6 +1468,13 @@
|
|||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/fast-memoize@1.2.6":
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.2.6.tgz#a442970db7e9634af556919343261a7bbe5e88c3"
|
||||
integrity sha512-9CWZ3+wCkClKHX+i5j+NyoBVqGf0pIskTo6Xl6ihGokYM2yqSSS68JIgeo+99UIHc+7vi9L3/SDSz/dWI9SNlA==
|
||||
dependencies:
|
||||
tslib "2.4.0"
|
||||
|
||||
"@formatjs/icu-messageformat-parser@2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz#a54293dd7f098d6a6f6a084ab08b6d54a3e8c12d"
|
||||
|
@ -1469,6 +1484,23 @@
|
|||
"@formatjs/icu-skeleton-parser" "1.3.6"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/icu-messageformat-parser@2.1.7":
|
||||
version "2.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.7.tgz#35dc556c13a0544cc730300c8ddb730ba7f44bd4"
|
||||
integrity sha512-KM4ikG5MloXMulqn39Js3ypuVzpPKq/DDplvl01PE2qD9rAzFO8YtaUCC9vr9j3sRXwdHPeTe8r3J/8IJgvYEQ==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.12.0"
|
||||
"@formatjs/icu-skeleton-parser" "1.3.13"
|
||||
tslib "2.4.0"
|
||||
|
||||
"@formatjs/icu-skeleton-parser@1.3.13":
|
||||
version "1.3.13"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.13.tgz#f7e186e72ed73c3272d22a3aacb646e77368b099"
|
||||
integrity sha512-qb1kxnA4ep76rV+d9JICvZBThBpK5X+nh1dLmmIReX72QyglicsaOmKEcdcbp7/giCWfhVs6CXPVA2JJ5/ZvAw==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.12.0"
|
||||
tslib "2.4.0"
|
||||
|
||||
"@formatjs/icu-skeleton-parser@1.3.6":
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz#4ce8c0737d6f07b735288177049e97acbf2e8964"
|
||||
|
@ -1477,6 +1509,24 @@
|
|||
"@formatjs/ecma402-abstract" "1.11.4"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/intl-displaynames@6.1.3":
|
||||
version "6.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-6.1.3.tgz#c9d283db518cd721c0855e9854bfadb9ba304b6a"
|
||||
integrity sha512-yBB165IH72fweGymRPrq8PQ4R5gKMR8vOj6XmkxGBICyJMhknc+RpG02g9Jsk/4jvO6qw/H0QtXHrHIg+Jv0sw==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.12.0"
|
||||
"@formatjs/intl-localematcher" "0.2.31"
|
||||
tslib "2.4.0"
|
||||
|
||||
"@formatjs/intl-listformat@7.1.2":
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-listformat/-/intl-listformat-7.1.2.tgz#3c5145436434795fa834150d0b6b6dc577aa6964"
|
||||
integrity sha512-WfWkJ8k41jZIhXgBtC2T1SpTSKYig99g9MVqrVRco4kduv/6GUWq1eMjk84qZfbU4rwdwc8qct+/gB6DTS17+w==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.12.0"
|
||||
"@formatjs/intl-localematcher" "0.2.31"
|
||||
tslib "2.4.0"
|
||||
|
||||
"@formatjs/intl-localematcher@0.2.25":
|
||||
version "0.2.25"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz#60892fe1b271ec35ba07a2eb018a2dd7bca6ea3a"
|
||||
|
@ -1484,6 +1534,26 @@
|
|||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/intl-localematcher@0.2.31":
|
||||
version "0.2.31"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.31.tgz#aada2b1e58211460cedba56889e3c489117eb6eb"
|
||||
integrity sha512-9QTjdSBpQ7wHShZgsNzNig5qT3rCPvmZogS/wXZzKotns5skbXgs0I7J8cuN0PPqXyynvNVuN+iOKhNS2eb+ZA==
|
||||
dependencies:
|
||||
tslib "2.4.0"
|
||||
|
||||
"@formatjs/intl@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-2.4.1.tgz#3e8ae8542e827c55cb1b7298bd72d4a009c2224d"
|
||||
integrity sha512-lWJ5dhLlkbMeWQOxBCq4MJNkB735TO5rwvcnnFzTx1H9Pkth1OLRH1R1aCAudptbd0Qe1W2hwJiMLumKpl6WCg==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.12.0"
|
||||
"@formatjs/fast-memoize" "1.2.6"
|
||||
"@formatjs/icu-messageformat-parser" "2.1.7"
|
||||
"@formatjs/intl-displaynames" "6.1.3"
|
||||
"@formatjs/intl-listformat" "7.1.2"
|
||||
intl-messageformat "10.1.4"
|
||||
tslib "2.4.0"
|
||||
|
||||
"@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
|
||||
|
@ -3220,7 +3290,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.2.tgz#0e670ea254d559241b6eeb3894f8754991e73220"
|
||||
integrity sha512-ui3WwXmjTaY73fOQ3/m3nnajU/Orhi6cEu5rzX+BrAAJxa3eITXZ5ch9suPqtM03OWhAHhPSyBGCN4UKoxO20Q==
|
||||
|
||||
"@types/hoist-non-react-statics@^3.3.0":
|
||||
"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
|
||||
|
@ -3318,6 +3388,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||
|
||||
"@types/json-to-ast@2.1.2":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-to-ast/-/json-to-ast-2.1.2.tgz#1f6670cfcd44eb745472744a3630b4da563f27dc"
|
||||
integrity sha512-GEjR5l9wZGS74KhL1a1tZuyRJqdLB7LGgOXzWspJx9xxC/iyCFTwwKv71Lz8fzZyGuVW8FjASQGoYFi6XZJWLQ==
|
||||
|
||||
"@types/json5@^0.0.29":
|
||||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
|
@ -3580,7 +3655,7 @@
|
|||
"@types/prop-types" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@17.0.45", "@types/react@^17":
|
||||
"@types/react@*", "@types/react@16 || 17 || 18", "@types/react@17.0.45", "@types/react@^17":
|
||||
version "17.0.45"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.45.tgz#9b3d5b661fd26365fefef0e766a1c6c30ccf7b3f"
|
||||
integrity sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg==
|
||||
|
@ -5927,6 +6002,14 @@ chai@4.3.4:
|
|||
pathval "^1.1.1"
|
||||
type-detect "^4.0.5"
|
||||
|
||||
chalk@4.1.2, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
|
||||
|
@ -5955,14 +6038,6 @@ chalk@^3.0.0:
|
|||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
character-entities-legacy@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.1.tgz#f40779df1a101872bb510a3d295e1fccf147202f"
|
||||
|
@ -6190,6 +6265,11 @@ clsx@^1.0.4:
|
|||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
|
||||
|
||||
code-error-fragment@0.0.230:
|
||||
version "0.0.230"
|
||||
resolved "https://registry.yarnpkg.com/code-error-fragment/-/code-error-fragment-0.0.230.tgz#d736d75c832445342eca1d1fedbf17d9618b14d7"
|
||||
integrity sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw==
|
||||
|
||||
code-point-at@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
|
@ -9600,6 +9680,11 @@ graceful-fs@^4.2.9:
|
|||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
||||
|
||||
grapheme-splitter@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
|
||||
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
|
||||
|
||||
growl@1.10.5:
|
||||
version "1.10.5"
|
||||
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
|
||||
|
@ -10345,6 +10430,16 @@ interpret@^2.2.0:
|
|||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
|
||||
integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
|
||||
|
||||
intl-messageformat@10.1.4:
|
||||
version "10.1.4"
|
||||
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.1.4.tgz#bf5ad48e357e3f3ab6559599296f54c175b22a92"
|
||||
integrity sha512-tXCmWCXhbeHOF28aIf5b9ce3kwdwGyIiiSXVZsyDwksMiGn5Tp0MrMvyeuHuz4uN1UL+NfGOztHmE+6aLFp1wQ==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.12.0"
|
||||
"@formatjs/fast-memoize" "1.2.6"
|
||||
"@formatjs/icu-messageformat-parser" "2.1.7"
|
||||
tslib "2.4.0"
|
||||
|
||||
intl-messageformat@^9.3.19:
|
||||
version "9.13.0"
|
||||
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-9.13.0.tgz#97360b73bd82212e4f6005c712a4a16053165468"
|
||||
|
@ -11146,6 +11241,14 @@ json-stringify-safe@^5.0.1:
|
|||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
|
||||
json-to-ast@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/json-to-ast/-/json-to-ast-2.1.0.tgz#041a9fcd03c0845036acb670d29f425cea4faaf9"
|
||||
integrity sha512-W9Lq347r8tA1DfMvAGn9QNcgYm4Wm7Yc+k8e6vezpMnRT+NHbtlxgNBXRVjXe9YM6eTn6+p/MKOlV/aABJcSnQ==
|
||||
dependencies:
|
||||
code-error-fragment "0.0.230"
|
||||
grapheme-splitter "^1.0.4"
|
||||
|
||||
json5@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-0.4.0.tgz#054352e4c4c80c86c0923877d449de176a732c8d"
|
||||
|
@ -14501,6 +14604,22 @@ react-inspector@^5.1.0:
|
|||
is-dom "^1.0.0"
|
||||
prop-types "^15.0.0"
|
||||
|
||||
react-intl@6.1.1:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-6.1.1.tgz#9c9b613f8de8a7d08311455d6a901806da005f8d"
|
||||
integrity sha512-nNNHBxivUdNwIcqNR1I4mLDAfDtnh1glEaOa8Sfu2pUDvKzYQyX6+in1PDcIn5RyV6enMgw9I6H+VwtlRDXhRw==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.12.0"
|
||||
"@formatjs/icu-messageformat-parser" "2.1.7"
|
||||
"@formatjs/intl" "2.4.1"
|
||||
"@formatjs/intl-displaynames" "6.1.3"
|
||||
"@formatjs/intl-listformat" "7.1.2"
|
||||
"@types/hoist-non-react-statics" "^3.3.1"
|
||||
"@types/react" "16 || 17 || 18"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
intl-messageformat "10.1.4"
|
||||
tslib "2.4.0"
|
||||
|
||||
react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
|
@ -16873,6 +16992,11 @@ tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0:
|
|||
minimist "^1.2.0"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@2.4.0, tslib@^2.0.0, tslib@^2.1.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
|
||||
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
||||
|
||||
tslib@^1.8.1, tslib@^1.9.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
||||
|
@ -16883,11 +17007,6 @@ tslib@^1.9.3:
|
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.1.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
|
||||
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
||||
|
||||
tslib@^2.0.1, tslib@^2.0.3:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
|
||||
|
|
Loading…
Reference in a new issue