Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui: Use "channel" or "stream" based on server feature level #5830

Merged
merged 2 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
58 changes: 56 additions & 2 deletions src/boot/TranslationProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import type { IntlShape } from 'react-intl';
import type { GetText } from '../types';
import { useGlobalSelector } from '../react-redux';
import { getGlobalSettings } from '../selectors';
import messages from '../i18n/messages';
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={messages[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?',
Comment on lines +33 to +36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW here's the command I used just now to help check this — a variation on the Perl one-liner that edited the messages JSON file:
<static/translations/messages_en.json perl -lne 'next unless (my ($k) = /^ *(".*"): \1/); $_ = $k; next unless (s/stream/channel/g || s/Stream/Channel/g); print "$k: $_,"'

I took that command's output, copy-pasted it between the braces of this object literal, deleted what had been here, and reformatted. The result was identical to what you have… except it added a line

  'No messages in topic: {streamAndTopic}': 'No messages in topic: {channelAndTopic}',

which indeed shouldn't be there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also occurs to me that that {streamAndTopic} string might be worth including but commented out, as a bit of a reminder of why this map isn't just a replaceAll or the like.

'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',
};
File renamed without changes.
4 changes: 2 additions & 2 deletions src/settings/languages.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* @flow strict-local */
import messages from '../i18n/messages';
import messagesByLanguage from '../i18n/messagesByLanguage';

export type Language = {|
tag: $Keys<typeof messages>,
tag: $Keys<typeof messagesByLanguage>,
name: string,
selfname: string,
|};
Expand Down
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
Loading