From 1dfc3e7f8dd43de3beba2f06325d128e64d752bc Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 1 Mar 2024 18:21:22 -0800 Subject: [PATCH] WIP ui: Use "channel" or "stream" based on server feature level TODO: - use the right feature level - Make sure servers at that feature level accept `@channel` for wildcard mentions. Discussion: https://chat.zulip.org/#narrow/stream/378-api-design/topic/stream.2Fchannel.20rename.3A.20.40stream.20wildcard.20mention/near/1752545 - write appropriate TODO(server-x) comments - run tools/tx-sync Done at the level of TranslationProvider, which (conveniently) can access the feature level of the active account if any. The messages_en.json changes were done with a handy Perl command that Greg helped me work out: $ perl -i -ne ' print; print if (s/stream/channel/g || s/Stream/Channel/g); ' static/translations/messages_en.json Fixes: #5827 --- src/autocomplete/PeopleAutocomplete.js | 2 + src/autocomplete/WildcardMentionItem.js | 32 +++++++-- src/boot/TranslationProvider.js | 55 +++++++++++++++- src/boot/streamChannelRenamesMap.js | 88 +++++++++++++++++++++++++ static/translations/messages_en.json | 41 ++++++++++++ 5 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 src/boot/streamChannelRenamesMap.js diff --git a/src/autocomplete/PeopleAutocomplete.js b/src/autocomplete/PeopleAutocomplete.js index a4a9acac97b..ae9b561a2d2 100644 --- a/src/autocomplete/PeopleAutocomplete.js +++ b/src/autocomplete/PeopleAutocomplete.js @@ -21,6 +21,7 @@ import WildcardMentionItem, { } from './WildcardMentionItem'; import { TranslationContext } from '../boot/TranslationProvider'; import { getZulipFeatureLevel } from '../account/accountsSelectors'; +import { streamChannelRenameFeatureLevel } from '../boot/streamChannelRenamesMap'; type Props = $ReadOnly<{| filter: string, @@ -74,6 +75,7 @@ export default function PeopleAutocomplete(props: Props): Node { destinationNarrow, // TODO(server-8.0) zulipFeatureLevel >= 224, + zulipFeatureLevel >= streamChannelRenameFeatureLevel, _, ); const filteredUsers = getAutocompleteSuggestion(users, filter, ownUserId, mutedUsers); diff --git a/src/autocomplete/WildcardMentionItem.js b/src/autocomplete/WildcardMentionItem.js index 7a1c78d2fc8..498f8a13892 100644 --- a/src/autocomplete/WildcardMentionItem.js +++ b/src/autocomplete/WildcardMentionItem.js @@ -11,6 +11,9 @@ import Touchable from '../common/Touchable'; import { createStyleSheet, ThemeContext } from '../styles'; import { caseNarrowDefault, isStreamOrTopicNarrow } from '../utils/narrow'; import { TranslationContext } from '../boot/TranslationProvider'; +import { useSelector } from '../react-redux'; +import { getZulipFeatureLevel } from '../account/accountsSelectors'; +import { streamChannelRenameFeatureLevel } from '../boot/streamChannelRenamesMap'; /** * A type of wildcard mention recognized by the server. @@ -38,14 +41,17 @@ export enum WildcardMentionType { // All of these should appear in messages_en.json so we can make the // wildcard mentions discoverable in the people autocomplete in the client's // own language. See getWildcardMentionsForQuery. -const englishCanonicalStringOf = (type: WildcardMentionType): string => { +const englishCanonicalStringOf = ( + type: WildcardMentionType, + useChannelTerminology: boolean, +): string => { switch (type) { case WildcardMentionType.All: return 'all'; case WildcardMentionType.Everyone: return 'everyone'; case WildcardMentionType.Stream: - return 'stream'; + return useChannelTerminology ? 'channel' : 'stream'; case WildcardMentionType.Topic: return 'topic'; } @@ -86,11 +92,20 @@ export const getWildcardMentionsForQuery = ( query: string, destinationNarrow: Narrow, topicMentionSupported: boolean, + useChannelTerminology: boolean, _: GetText, ): $ReadOnlyArray => { const queryMatchesWildcard = (type: WildcardMentionType): boolean => - typeahead.query_matches_string(query, serverCanonicalStringOf(type), ' ') - || typeahead.query_matches_string(query, _(englishCanonicalStringOf(type)), ' '); + typeahead.query_matches_string( + query, + serverCanonicalStringOf(type, useChannelTerminology), + ' ', + ) + || typeahead.query_matches_string( + query, + _(englishCanonicalStringOf(type, useChannelTerminology)), + ' ', + ); const results = []; @@ -135,9 +150,12 @@ export default function WildcardMentionItem(props: Props): Node { const _ = useContext(TranslationContext); + const zulipFeatureLevel = useSelector(getZulipFeatureLevel); + const useChannelTerminology = zulipFeatureLevel >= streamChannelRenameFeatureLevel; + const handlePress = useCallback(() => { - onPress(type, serverCanonicalStringOf(type)); - }, [onPress, type]); + onPress(type, serverCanonicalStringOf(type, useChannelTerminology)); + }, [onPress, type, useChannelTerminology]); const themeContext = useContext(ThemeContext); @@ -179,7 +197,7 @@ export default function WildcardMentionItem(props: Props): Node { diff --git a/src/boot/TranslationProvider.js b/src/boot/TranslationProvider.js index 7c43c3ad68b..74acd37f43a 100644 --- a/src/boot/TranslationProvider.js +++ b/src/boot/TranslationProvider.js @@ -8,6 +8,13 @@ import type { GetText } from '../types'; import { useGlobalSelector } from '../react-redux'; import { getGlobalSettings } from '../selectors'; import messagesByLanguage from '../i18n/messagesByLanguage'; +import { getZulipFeatureLevel, tryGetActiveAccountState } from '../account/accountsSelectors'; +import { objectFromEntries } from '../jsBackport'; +import { objectEntries } from '../flowPonyfill'; +import { + streamChannelRenameFeatureLevel, + streamChannelRenamesMap, +} from './streamChannelRenamesMap'; // $FlowFixMe[incompatible-type] could put a well-typed mock value here, to help write tests export const TranslationContext: React.Context = React.createContext(undefined); @@ -53,12 +60,58 @@ type Props = $ReadOnly<{| children: React.Node, |}>; +/** + * Like messagesByLanguage but with "channel" terminology instead of "stream". + */ +const messagesByLanguageRenamed = objectFromEntries( + objectEntries(messagesByLanguage).map(([language, messages]) => [ + language, + objectFromEntries( + objectEntries(messages).map(([messageId, message]) => { + const renamedMessageId = streamChannelRenamesMap[messageId]; + if (renamedMessageId == null) { + return [messageId, message]; + } + + const renamedMessage = messages[renamedMessageId]; + if (renamedMessage === renamedMessageId && message !== messageId) { + // The newfangled "channel" string hasn't been translated yet, but + // the older "stream" string has. Consider falling back to that. + if (/^en($|-)/.test(language)) { + // The language is a variety of English. Prefer the newer + // terminology, even though awaiting translation. (Most of our + // strings don't change at all between one English and another.) + return [messageId, renamedMessage]; + } + // Use the translation we have, even of the older terminology. + // (In many languages the translations have used an equivalent + // of "channel" all along anyway.) + return [messageId, message]; + } + return [messageId, renamedMessage]; + }), + ), + ]), +); + export default function TranslationProvider(props: Props): React.Node { const { children } = props; const language = useGlobalSelector(state => getGlobalSettings(state).language); + const activeAccountState = useGlobalSelector(tryGetActiveAccountState); + + const effectiveMessagesByLanguage = + activeAccountState == null + || getZulipFeatureLevel(activeAccountState) > streamChannelRenameFeatureLevel + ? messagesByLanguageRenamed + : messagesByLanguage; + return ( - + {children} ); diff --git a/src/boot/streamChannelRenamesMap.js b/src/boot/streamChannelRenamesMap.js new file mode 100644 index 00000000000..4d6f106f705 --- /dev/null +++ b/src/boot/streamChannelRenamesMap.js @@ -0,0 +1,88 @@ +/* @flow strict-local */ + +/** + * The feature level at which we want to say "channel" instead of "stream". + * + * Outside a per-account context, check the feature level of the active + * account, if there is one. If there isn't an active account, just choose + * "channel" terminology unconditionally. + */ +export const streamChannelRenameFeatureLevel = 1; // TODO + +/** + * A messageId: messageId map, from "stream" terminology to "channel". + * + * When appropriate (see streamChannelRenameFeatureLevel), use this to patch + * UI-string data for all languages, so that the UI says "channel" instead + * of "stream". See https://github.com/zulip/zulip-mobile/issues/5827 . + * + * For example, use this to make a copy of messages_en that has + * + * "Notify stream": "Notify channel", + * + * instead of + * + * "Notify stream": "Notify stream", + * "Notify channel": "Notify channel", + * + * and likewise for all the other languages. + */ +export const streamChannelRenamesMap: {| [string]: string |} = { + stream: 'channel', + 'Notify stream': 'Notify channel', + 'Who can access the stream?': 'Who can access the channel?', + 'Only organization administrators and owners can edit streams.': + 'Only organization administrators and owners can edit channels.', + '{realmName} only allows organization administrators or owners to make public streams.': + '{realmName} only allows organization administrators or owners to make public channels.', + '{realmName} only allows organization moderators, administrators, or owners to make public streams.': + '{realmName} only allows organization moderators, administrators, or owners to make public channels.', + '{realmName} only allows full organization members, moderators, administrators, or owners to make public streams.': + '{realmName} only allows full organization members, moderators, administrators, or owners to make public channels.', + '{realmName} only allows organization members, moderators, administrators, or owners to make public streams.': + '{realmName} only allows organization members, moderators, administrators, or owners to make public channels.', + '{realmName} only allows organization administrators or owners to make private streams.': + '{realmName} only allows organization administrators or owners to make private channels.', + '{realmName} only allows organization moderators, administrators, or owners to make private streams.': + '{realmName} only allows organization moderators, administrators, or owners to make private channels.', + '{realmName} only allows full organization members, moderators, administrators, or owners to make private streams.': + '{realmName} only allows full organization members, moderators, administrators, or owners to make private channels.', + '{realmName} only allows organization members, moderators, administrators, or owners to make private streams.': + '{realmName} only allows organization members, moderators, administrators, or owners to make private channels.', + '{realmName} does not allow anybody to make web-public streams.': + '{realmName} does not allow anybody to make web-public channels.', + '{realmName} only allows organization owners to make web-public streams.': + '{realmName} only allows organization owners to make web-public channels.', + '{realmName} only allows organization administrators or owners to make web-public streams.': + '{realmName} only allows organization administrators or owners to make web-public channels.', + '{realmName} only allows organization moderators, administrators, or owners to make web-public streams.': + '{realmName} only allows organization moderators, administrators, or owners to make web-public channels.', + 'Cannot subscribe to stream': 'Cannot subscribe to channel', + 'Stream #{name} is private.': 'Channel #{name} is private.', + 'Please specify a stream.': 'Please specify a channel.', + 'Please specify a valid stream.': 'Please specify a valid channel.', + 'No messages in stream': 'No messages in channel', + 'All streams': 'All channels', + // 'No messages in topic: {streamAndTopic}': 'No messages in topic: {channelAndTopic}', + 'Mute stream': 'Mute channel', + 'Unmute stream': 'Unmute channel', + '{username} will not be notified unless you subscribe them to this stream.': + '{username} will not be notified unless you subscribe them to this channel.', + 'Stream notifications': 'Channel notifications', + 'No streams found': 'No channels found', + 'Mark stream as read': 'Mark channel as read', + 'Failed to mute stream': 'Failed to mute channel', + 'Failed to unmute stream': 'Failed to unmute channel', + 'Stream settings': 'Channel settings', + 'Failed to show stream settings': 'Failed to show channel settings', + 'You are not subscribed to this stream': 'You are not subscribed to this channel', + 'Create new stream': 'Create new channel', + Stream: 'Channel', + 'Edit stream': 'Edit channel', + 'Only organization admins are allowed to post to this stream.': + 'Only organization admins are allowed to post to this channel.', + 'Copy link to stream': 'Copy link to channel', + 'Failed to copy stream link': 'Failed to copy channel link', + 'A stream with this name already exists.': 'A channel with this name already exists.', + Streams: 'Channels', +}; diff --git a/static/translations/messages_en.json b/static/translations/messages_en.json index 9bf6c8a5358..e26e46f1360 100644 --- a/static/translations/messages_en.json +++ b/static/translations/messages_en.json @@ -15,8 +15,10 @@ "all": "all", "everyone": "everyone", "stream": "stream", + "channel": "channel", "topic": "topic", "Notify stream": "Notify stream", + "Notify channel": "Notify channel", "Notify topic": "Notify topic", "Notify recipients": "Notify recipients", "Notify everyone": "Notify everyone", @@ -46,30 +48,46 @@ "Discard changes": "Discard changes", "You have unsaved changes. Leave without saving?": "You have unsaved changes. Leave without saving?", "Who can access the stream?": "Who can access the stream?", + "Who can access the channel?": "Who can access the channel?", "Cannot apply requested settings": "Cannot apply requested settings", "Insufficient permission": "Insufficient permission", "Web-public": "Web-public", "Organization members can join (guests must be invited by a subscriber); anyone on the Internet can view complete message history without creating an account": "Organization members can join (guests must be invited by a subscriber); anyone on the Internet can view complete message history without creating an account", "Only organization administrators and owners can edit streams.": "Only organization administrators and owners can edit streams.", + "Only organization administrators and owners can edit channels.": "Only organization administrators and owners can edit channels.", "{realmName} only allows organization administrators or owners to make public streams.": "{realmName} only allows organization administrators or owners to make public streams.", + "{realmName} only allows organization administrators or owners to make public channels.": "{realmName} only allows organization administrators or owners to make public channels.", "{realmName} only allows organization moderators, administrators, or owners to make public streams.": "{realmName} only allows organization moderators, administrators, or owners to make public streams.", + "{realmName} only allows organization moderators, administrators, or owners to make public channels.": "{realmName} only allows organization moderators, administrators, or owners to make public channels.", "{realmName} only allows full organization members, moderators, administrators, or owners to make public streams.": "{realmName} only allows full organization members, moderators, administrators, or owners to make public streams.", + "{realmName} only allows full organization members, moderators, administrators, or owners to make public channels.": "{realmName} only allows full organization members, moderators, administrators, or owners to make public channels.", "{realmName} only allows organization members, moderators, administrators, or owners to make public streams.": "{realmName} only allows organization members, moderators, administrators, or owners to make public streams.", + "{realmName} only allows organization members, moderators, administrators, or owners to make public channels.": "{realmName} only allows organization members, moderators, administrators, or owners to make public channels.", "{realmName} only allows organization administrators or owners to make private streams.": "{realmName} only allows organization administrators or owners to make private streams.", + "{realmName} only allows organization administrators or owners to make private channels.": "{realmName} only allows organization administrators or owners to make private channels.", "{realmName} only allows organization moderators, administrators, or owners to make private streams.": "{realmName} only allows organization moderators, administrators, or owners to make private streams.", + "{realmName} only allows organization moderators, administrators, or owners to make private channels.": "{realmName} only allows organization moderators, administrators, or owners to make private channels.", "{realmName} only allows full organization members, moderators, administrators, or owners to make private streams.": "{realmName} only allows full organization members, moderators, administrators, or owners to make private streams.", + "{realmName} only allows full organization members, moderators, administrators, or owners to make private channels.": "{realmName} only allows full organization members, moderators, administrators, or owners to make private channels.", "{realmName} only allows organization members, moderators, administrators, or owners to make private streams.": "{realmName} only allows organization members, moderators, administrators, or owners to make private streams.", + "{realmName} only allows organization members, moderators, administrators, or owners to make private channels.": "{realmName} only allows organization members, moderators, administrators, or owners to make private channels.", "{realmName} does not allow anybody to make web-public streams.": "{realmName} does not allow anybody to make web-public streams.", + "{realmName} does not allow anybody to make web-public channels.": "{realmName} does not allow anybody to make web-public channels.", "{realmName} only allows organization owners to make web-public streams.": "{realmName} only allows organization owners to make web-public streams.", + "{realmName} only allows organization owners to make web-public channels.": "{realmName} only allows organization owners to make web-public channels.", "{realmName} only allows organization administrators or owners to make web-public streams.": "{realmName} only allows organization administrators or owners to make web-public streams.", + "{realmName} only allows organization administrators or owners to make web-public channels.": "{realmName} only allows organization administrators or owners to make web-public channels.", "{realmName} only allows organization moderators, administrators, or owners to make web-public streams.": "{realmName} only allows organization moderators, administrators, or owners to make web-public streams.", + "{realmName} only allows organization moderators, administrators, or owners to make web-public channels.": "{realmName} only allows organization moderators, administrators, or owners to make web-public channels.", "Organization members can join (guests must be invited by a subscriber); organization members can view complete message history without joining": "Organization members can join (guests must be invited by a subscriber); organization members can view complete message history without joining", "Private, shared history": "Private, shared history", "Must be invited by a subscriber; new subscribers can view complete message history; hidden from non-administrator users": "Must be invited by a subscriber; new subscribers can view complete message history; hidden from non-administrator users", "Private, protected history": "Private, protected history", "Must be invited by a subscriber; new subscribers can only see messages sent after they join; hidden from non-administrator users": "Must be invited by a subscriber; new subscribers can only see messages sent after they join; hidden from non-administrator users", "Cannot subscribe to stream": "Cannot subscribe to stream", + "Cannot subscribe to channel": "Cannot subscribe to channel", "Stream #{name} is private.": "Stream #{name} is private.", + "Channel #{name} is private.": "Channel #{name} is private.", "See details": "See details", "Failed to show details": "Failed to show details", "Share canceled": "Share canceled", @@ -100,7 +118,9 @@ "Message not sent": "Message not sent", "Please specify a topic.": "Please specify a topic.", "Please specify a stream.": "Please specify a stream.", + "Please specify a channel.": "Please specify a channel.", "Please specify a valid stream.": "Please specify a valid stream.", + "Please specify a valid channel.": "Please specify a valid channel.", "Please choose recipients.": "Please choose recipients.", "Message is empty.": "Message is empty.", "Could not connect": "Could not connect", @@ -125,6 +145,7 @@ "No messages": "No messages", "No messages on server": "No messages on server", "No messages in stream": "No messages in stream", + "No messages in channel": "No messages in channel", "No messages with this topic": "No messages with this topic", "No messages with this person": "No messages with this person", "No messages in this group": "No messages in this group", @@ -141,6 +162,7 @@ "This will log out {email} on {realmUrl} from the mobile app on this device.": "This will log out {email} on {realmUrl} from the mobile app on this device.", "Add new account": "Add new account", "All streams": "All streams", + "All channels": "All channels", "Email": "Email", "Username": "Username", "Password": "Password", @@ -178,7 +200,9 @@ "Unfollow topic": "Unfollow topic", "Failed to unfollow topic": "Failed to unfollow topic", "Mute stream": "Mute stream", + "Mute channel": "Mute channel", "Unmute stream": "Unmute stream", + "Unmute channel": "Unmute channel", "No Internet connection": "No Internet connection", "Please check your Internet connection": "Please check your Internet connection", "Zulip’s Internet connection is uncertain.": "Zulip’s Internet connection is uncertain.", @@ -252,6 +276,7 @@ "Jot down something": "Jot down something", "Message {recipient}": "Message {recipient}", "{username} will not be notified unless you subscribe them to this stream.": "{username} will not be notified unless you subscribe them to this stream.", + "{username} will not be notified unless you subscribe them to this channel.": "{username} will not be notified unless you subscribe them to this channel.", "Send direct message": "Send direct message", "View direct messages": "View direct messages", "(This user has been deactivated)": "(This user has been deactivated)", @@ -288,11 +313,14 @@ "Notifications when online": "Notifications when online", "Notifications when offline": "Notifications when offline", "Stream notifications": "Stream notifications", + "Channel notifications": "Channel notifications", "No users found": "No users found", "No streams found": "No streams found", + "No channels found": "No channels found", "Legal": "Legal", "Mark all as read": "Mark all as read", "Mark stream as read": "Mark stream as read", + "Mark channel as read": "Mark channel as read", "Mark topic as read": "Mark topic as read", "Mark conversation as read": "Mark conversation as read", "Mark as unread from here": "Mark as unread from here", @@ -321,11 +349,15 @@ "Failed to unmute topic": "Failed to unmute topic", "Failed to mute topic": "Failed to mute topic", "Failed to mute stream": "Failed to mute stream", + "Failed to mute channel": "Failed to mute channel", "Failed to unmute stream": "Failed to unmute stream", + "Failed to unmute channel": "Failed to unmute channel", "Failed to delete topic": "Failed to delete topic", "Failed to mark as unread": "Failed to mark as unread", "Stream settings": "Stream settings", + "Channel settings": "Channel settings", "Failed to show stream settings": "Failed to show stream settings", + "Failed to show channel settings": "Failed to show channel settings", "show": "show", "hide": "hide", "Administrators": "Administrators", @@ -333,7 +365,9 @@ "Share image": "Share image", "Share link to image": "Share link to image", "You are not subscribed to this stream": "You are not subscribed to this stream", + "You are not subscribed to this channel": "You are not subscribed to this channel", "Create new stream": "Create new stream", + "Create new channel": "Create new channel", "Name": "Name", "Description": "Description", "Create": "Create", @@ -341,8 +375,10 @@ "Public": "Public", "Notifications": "Notifications", "Stream": "Stream", + "Channel": "Channel", "Direct message": "Direct message", "Edit stream": "Edit stream", + "Edit channel": "Edit channel", "OK": "OK", "Please share the image from your browser": "Please share the image from your browser", "Share failed": "Share failed", @@ -367,6 +403,7 @@ "{userFullName} (guest)": "{userFullName} (guest)", "{userFullName} (guest)": "{userFullName} (guest)", "Only organization admins are allowed to post to this stream.": "Only organization admins are allowed to post to this stream.", + "Only organization admins are allowed to post to this channel.": "Only organization admins are allowed to post to this channel.", "Connecting...": "Connecting...", "Set your status": "Set your status", "Muted": "Muted", @@ -402,9 +439,13 @@ "Copy link to topic": "Copy link to topic", "Failed to copy topic link": "Failed to copy topic link", "Copy link to stream": "Copy link to stream", + "Copy link to channel": "Copy link to channel", "Failed to copy stream link": "Failed to copy stream link", + "Failed to copy channel link": "Failed to copy channel link", "A stream with this name already exists.": "A stream with this name already exists.", + "A channel with this name already exists.": "A channel with this name already exists.", "Streams": "Streams", + "Channels": "Channels", "Owner": "Owner", "Admin": "Admin", "Moderator": "Moderator",