Skip to content

Commit

Permalink
ui: Use "channel" or "stream" based on server feature level
Browse files Browse the repository at this point in the history
Done at the level of TranslationProvider, which (conveniently) can
access the feature level of the active account if any.

Fixes: #5827
  • Loading branch information
chrisbobbe authored and gnprice committed Apr 26, 2024
1 parent 4bbf876 commit 7c35635
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 14 deletions.
2 changes: 2 additions & 0 deletions src/autocomplete/PeopleAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
33 changes: 26 additions & 7 deletions src/autocomplete/WildcardMentionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -38,14 +41,18 @@ 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';
// TODO(server-9.0) remove "stream" terminology
return useChannelTerminology ? 'channel' : 'stream';
case WildcardMentionType.Topic:
return 'topic';
}
Expand Down Expand Up @@ -86,11 +93,20 @@ export const getWildcardMentionsForQuery = (
query: string,
destinationNarrow: Narrow,
topicMentionSupported: boolean,
useChannelTerminology: boolean,
_: GetText,
): $ReadOnlyArray<WildcardMentionType> => {
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 = [];

Expand Down Expand Up @@ -135,9 +151,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);

Expand Down Expand Up @@ -179,7 +198,7 @@ export default function WildcardMentionItem(props: Props): Node {
<View style={styles.textWrapper}>
<ZulipText
style={styles.text}
text={serverCanonicalStringOf(type)}
text={serverCanonicalStringOf(type, useChannelTerminology)}
numberOfLines={1}
ellipsizeMode="tail"
/>
Expand Down
56 changes: 55 additions & 1 deletion src/boot/TranslationProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<GetText> = React.createContext(undefined);
Expand Down Expand Up @@ -53,12 +60,59 @@ 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);

// TODO(server-9.0) remove "stream" terminology
const effectiveMessagesByLanguage =
activeAccountState == null
|| getZulipFeatureLevel(activeAccountState) > streamChannelRenameFeatureLevel
? messagesByLanguageRenamed
: messagesByLanguage;

return (
<IntlProvider locale={language} textComponent={Text} messages={messagesByLanguage[language]}>
<IntlProvider
locale={language}
textComponent={Text}
messages={effectiveMessagesByLanguage[language]}
>
<TranslationContextTranslator>{children}</TranslationContextTranslator>
</IntlProvider>
);
Expand Down
91 changes: 91 additions & 0 deletions src/boot/streamChannelRenamesMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* @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.
*/
// TODO(server-9.0) simplify away
// https://chat.zulip.org/api/changelog#changes-in-zulip-90
export const streamChannelRenameFeatureLevel = 255;

/**
* 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.
*/
// TODO(server-9.0) simplify away
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',
};
9 changes: 3 additions & 6 deletions tools/check-messages-en
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,9 @@ function main() {

// Check each key ("message ID" in formatjs's lingo) against
// possibleUiStrings, and make a list of any that aren't found.
const danglingMessageIds = Object.keys(messages_en)
.filter(messageId => !possibleUiStrings.has(messageId))
// Ignore some UI strings that we want to offer to our translators
// but that aren't yet ready to show in the UI (although we intend to).
// TODO(#5827) Stop filtering out messages with "channel" terminology
.filter(messageId => !/[cC]hannel/.test(messageId));
const danglingMessageIds = Object.keys(messages_en).filter(
messageId => !possibleUiStrings.has(messageId),
);

if (danglingMessageIds.length > 0) {
console.error(
Expand Down

0 comments on commit 7c35635

Please sign in to comment.