From d81f71f993a683488a115dd47b902fa97744b13b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 21 Aug 2023 09:15:22 +0100 Subject: [PATCH] Load SAS Emoji translations from `@matrix-org/spec` (#11429) * Remove last instance of componentWillMount * Load SAS Emoji translations from @matrix-org/spec * Fix import * Test normalisation on both sides * update comment for @richvdh * Delint --- docs/settings.md | 2 +- package.json | 1 + .../verification/VerificationShowSas.tsx | 132 +++++++----------- src/i18n/strings/en_EN.json | 63 --------- .../views/VerificationShowSas-test.tsx | 31 ++++ yarn.lock | 5 + 6 files changed, 87 insertions(+), 147 deletions(-) create mode 100644 test/components/views/VerificationShowSas-test.tsx diff --git a/docs/settings.md b/docs/settings.md index b8a575ff5a..3f0636d380 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -174,7 +174,7 @@ An example of a watcher in action would be: class MyComponent extends React.Component { settingWatcherRef = null; - componentWillMount() { + componentDidMount() { const callback = (settingName, roomId, level, newValAtLevel, newVal) => { this.setState({ color: newVal }); }; diff --git a/package.json b/package.json index e25b821516..c923e50678 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@matrix-org/analytics-events": "^0.6.0", "@matrix-org/matrix-wysiwyg": "^2.4.1", "@matrix-org/react-sdk-module-api": "^1.0.0", + "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", diff --git a/src/components/views/verification/VerificationShowSas.tsx b/src/components/views/verification/VerificationShowSas.tsx index 67f137dde7..fd24acef9f 100644 --- a/src/components/views/verification/VerificationShowSas.tsx +++ b/src/components/views/verification/VerificationShowSas.tsx @@ -16,9 +16,10 @@ limitations under the License. import React from "react"; import { Device } from "matrix-js-sdk/src/matrix"; -import { GeneratedSas } from "matrix-js-sdk/src/crypto-api/verification"; +import { GeneratedSas, EmojiMapping } from "matrix-js-sdk/src/crypto-api/verification"; +import SasEmoji from "@matrix-org/spec/sas-emoji.json"; -import { _t, _td } from "../../../languageHandler"; +import { _t, getNormalizedLanguageKeys, getUserLanguage } from "../../../languageHandler"; import { PendingActionSpinner } from "../right_panel/EncryptionInfo"; import AccessibleButton from "../elements/AccessibleButton"; import { fixupColorFonts } from "../../../utils/FontManager"; @@ -42,19 +43,50 @@ interface IState { cancelling?: boolean; } -/** Convert the names of emojis returned by the js-sdk into the display names, which we use as - * a base for our translations. +const SasEmojiMap = new Map< + string, // lowercase + { + description: string; + translations: { + [normalizedLanguageKey: string]: string; + }; + } +>( + SasEmoji.map(({ description, translated_descriptions: translations }) => [ + description.toLowerCase(), + { + description, + // Normalize the translation keys + translations: Object.keys(translations).reduce>((o, k) => { + for (const key of getNormalizedLanguageKeys(k)) { + o[key] = translations[k as keyof typeof translations]!; + } + return o; + }, {}), + }, + ]), +); + +/** + * Translate given EmojiMapping into the target locale + * @param mapping - the given EmojiMapping to translate + * @param locale - the BCP 47 locale to translate to, will fall back to English as the base locale for Matrix SAS Emoji. */ -function capFirst(s: string): string { - // Our translations (currently) have names like "Thumbs up". - // - // With legacy crypto, the js-sdk returns lower-case names ("thumbs up"). With Rust crypto, the js-sdk follows - // the spec and returns title-case names ("Thumbs Up"). So, to convert both into names that match our i18n data, - // we upcase the first character and downcase the rest. - // - // Once legacy crypto is dead, we could consider getting rid of this and just making the i18n data use the - // title-case names (which would also match the spec). - return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(); +export function tEmoji(mapping: EmojiMapping, locale: string): string { + const name = mapping[1]; + const emoji = SasEmojiMap.get(name.toLowerCase()); + if (!emoji) { + console.warn("Emoji not found for translation", name); + return name; + } + + for (const key of getNormalizedLanguageKeys(locale)) { + if (!!emoji.translations[key]) { + return emoji.translations[key]; + } + } + + return emoji.description; } export default class VerificationShowSas extends React.Component { @@ -64,9 +96,7 @@ export default class VerificationShowSas extends React.Component this.state = { pending: false, }; - } - public componentWillMount(): void { // As this component is also used before login (during complete security), // also make sure we have a working emoji font to display the SAS emojis here. // This is also done from LoggedInView. @@ -84,13 +114,15 @@ export default class VerificationShowSas extends React.Component }; public render(): React.ReactNode { + const locale = getUserLanguage(); + let sasDisplay; let sasCaption; if (this.props.sas.emoji) { const emojiBlocks = this.props.sas.emoji.map((emoji, i) => (
{emoji[0]}
-
{_t(capFirst(emoji[1]))}
+
{tEmoji(emoji, locale)}
)); sasDisplay = ( @@ -171,69 +203,3 @@ export default class VerificationShowSas extends React.Component ); } } - -// List of Emoji strings from the js-sdk, for i18n -_td("Dog"); -_td("Cat"); -_td("Lion"); -_td("Horse"); -_td("Unicorn"); -_td("Pig"); -_td("Elephant"); -_td("Rabbit"); -_td("Panda"); -_td("Rooster"); -_td("Penguin"); -_td("Turtle"); -_td("Fish"); -_td("Octopus"); -_td("Butterfly"); -_td("Flower"); -_td("Tree"); -_td("Cactus"); -_td("Mushroom"); -_td("Globe"); -_td("Moon"); -_td("Cloud"); -_td("Fire"); -_td("Banana"); -_td("Apple"); -_td("Strawberry"); -_td("Corn"); -_td("Pizza"); -_td("Cake"); -_td("Heart"); -_td("Smiley"); -_td("Robot"); -_td("Hat"); -_td("Glasses"); -_td("Spanner"); -_td("Santa"); -_td("Thumbs up"); -_td("Umbrella"); -_td("Hourglass"); -_td("Clock"); -_td("Gift"); -_td("Light bulb"); -_td("Book"); -_td("Pencil"); -_td("Paperclip"); -_td("Scissors"); -_td("Lock"); -_td("Key"); -_td("Hammer"); -_td("Telephone"); -_td("Flag"); -_td("Train"); -_td("Bicycle"); -_td("Aeroplane"); -_td("Rocket"); -_td("Trophy"); -_td("Ball"); -_td("Guitar"); -_td("Trumpet"); -_td("Bell"); -_td("Anchor"); -_td("Headphones"); -_td("Folder"); -_td("Pin"); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7ce2a44ec6..64a3bb662e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1221,69 +1221,6 @@ "They don't match": "They don't match", "They match": "They match", "To be secure, do this in person or use a trusted way to communicate.": "To be secure, do this in person or use a trusted way to communicate.", - "Dog": "Dog", - "Cat": "Cat", - "Lion": "Lion", - "Horse": "Horse", - "Unicorn": "Unicorn", - "Pig": "Pig", - "Elephant": "Elephant", - "Rabbit": "Rabbit", - "Panda": "Panda", - "Rooster": "Rooster", - "Penguin": "Penguin", - "Turtle": "Turtle", - "Fish": "Fish", - "Octopus": "Octopus", - "Butterfly": "Butterfly", - "Flower": "Flower", - "Tree": "Tree", - "Cactus": "Cactus", - "Mushroom": "Mushroom", - "Globe": "Globe", - "Moon": "Moon", - "Cloud": "Cloud", - "Fire": "Fire", - "Banana": "Banana", - "Apple": "Apple", - "Strawberry": "Strawberry", - "Corn": "Corn", - "Pizza": "Pizza", - "Cake": "Cake", - "Heart": "Heart", - "Smiley": "Smiley", - "Robot": "Robot", - "Hat": "Hat", - "Glasses": "Glasses", - "Spanner": "Spanner", - "Santa": "Santa", - "Thumbs up": "Thumbs up", - "Umbrella": "Umbrella", - "Hourglass": "Hourglass", - "Clock": "Clock", - "Gift": "Gift", - "Light bulb": "Light bulb", - "Book": "Book", - "Pencil": "Pencil", - "Paperclip": "Paperclip", - "Scissors": "Scissors", - "Lock": "Lock", - "Key": "Key", - "Hammer": "Hammer", - "Telephone": "Telephone", - "Flag": "Flag", - "Train": "Train", - "Bicycle": "Bicycle", - "Aeroplane": "Aeroplane", - "Rocket": "Rocket", - "Trophy": "Trophy", - "Ball": "Ball", - "Guitar": "Guitar", - "Trumpet": "Trumpet", - "Bell": "Bell", - "Anchor": "Anchor", - "Headphones": "Headphones", - "Folder": "Folder", "Welcome": "Welcome", "Secure messaging for friends and family": "Secure messaging for friends and family", "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.": "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.", diff --git a/test/components/views/VerificationShowSas-test.tsx b/test/components/views/VerificationShowSas-test.tsx new file mode 100644 index 0000000000..f140de78a2 --- /dev/null +++ b/test/components/views/VerificationShowSas-test.tsx @@ -0,0 +1,31 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { EmojiMapping } from "matrix-js-sdk/src/crypto-api/verification"; + +import { tEmoji } from "../../../src/components/views/verification/VerificationShowSas"; + +describe("tEmoji", () => { + it.each([ + ["en-GB", "Dog"], + ["en", "Dog"], + ["de-DE", "Hund"], + ["pt", "Cachorro"], + ])("should handle locale %s", (locale, expectation) => { + const emoji: EmojiMapping = ["🐶", "Dog"]; + expect(tEmoji(emoji, locale)).toEqual(expectation); + }); +}); diff --git a/yarn.lock b/yarn.lock index d40f0ced66..1576c74356 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1874,6 +1874,11 @@ dependencies: "@babel/runtime" "^7.17.9" +"@matrix-org/spec@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@matrix-org/spec/-/spec-1.7.0.tgz#8a6b93edf0d99f8a6e0a25eea8613b5ada3e6b56" + integrity sha512-sLRdmk64dNd7X+jXgWFEatJbf2BOFX/a1VxHqWWTerzZntKsjKzz42sD2Mj1QWrsGp01u99fRNU8oy4DcmFn3w== + "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b"