We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Show info about organization and Zulip in gear menu
At present, it requires some digging to find a few key pieces of information about the Zulip organization one is viewing:

- Organization name
- Organization URL (if using the Desktop app)
- For Zulip Cloud, the plan that the organization on.
- For self-hosted Zulip, does the server need to be upgraded?  What's the version of the server, and what's the current release version?

We should address this by adding this information at the top of the gear settings menu.

- For all users: Show organization name and URL
- For all Zulip Cloud users: Show plan name with a link to `/plans`, e.g. "Zulip Cloud Free"
- For owners of Zulip Cloud Free orgs: Show "Upgrade to Zulip Cloud Standard" link to `/upgrade`
- For all self-hosted users:
   - Show Zulip server version (same as in the "About Zulip" widget); we'll need to test to make sure it looks reasonable for non-standard versions (e.g. forks, installs running off `main`).
   - If the server version is old, we should show an "Upgrade to the latest release (x.y)" linking to https://zulip.readthedocs.io/en/stable/production/upgrade-or-modify.html. We should probably show this link to all users, as server admins might not be owners/admins of the organization.

## Mockups
<img width="1552" alt="popover-menu" src="https://user-images.githubusercontent.com/2090066/172440944-5dc8ee48-908f-4642-beb7-9ec141128a29.png">

<img width="1552" alt="dark-inbox-01" src="https://user-images.githubusercontent.com/2090066/172440973-12639e2a-3f42-408d-b976-27b01498ecda.png">

<img width="1608" alt="selfhosted-upgrade" src="https://user-images.githubusercontent.com/2090066/172441028-c0ce417f-e3db-4542-845f-10ba3fab98df.png">

**CZO discussion threads:**
- [Design proposal (Zulip Cloud)](https://chat.zulip.org/#narrow/stream/101-design/topic/UI.20redesign.3A.20popover.20menu/near/1388585)
- [Server upgrade notice](https://chat.zulip.org/#narrow/stream/101-design/topic/server.20upgrade.20notice)



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `tools/lib/capitalization.py`
Content:
```
1 import re
2 from typing import List, Match, Tuple
3 
4 from bs4 import BeautifulSoup
5 
6 # The phrases in this list will be ignored. The longest phrase is
7 # tried first; this removes the chance of smaller phrases changing
8 # the text before longer phrases are tried.
9 # The errors shown by `tools/check-capitalization` can be added to
10 # this list without any modification.
11 IGNORED_PHRASES = [
12     # Proper nouns and acronyms
13     r"API",
14     r"APNS",
15     r"Botserver",
16     r"Cookie Bot",
17     r"DevAuthBackend",
18     r"GCM",
19     r"GitHub",
20     r"Gravatar",
21     r"Help Center",
22     r"HTTP",
23     r"ID",
24     r"IDs",
25     r"IP",
26     r"JSON",
27     r"Kerberos",
28     r"LDAP",
29     r"Markdown",
30     r"OTP",
31     r"Pivotal",
32     r"PM",
33     r"PMs",
34     r"Slack",
35     r"Google",
36     r"Terms of Service",
37     r"Tuesday",
38     r"URL",
39     r"UUID",
40     r"Webathena",
41     r"WordPress",
42     r"Zephyr",
43     r"Zoom",
44     r"Zulip",
45     r"Zulip Account Security",
46     r"Zulip Security",
47     r"Zulip Cloud Standard",
48     r"BigBlueButton",
49     # Code things
50     r"\.zuliprc",
51     # BeautifulSoup will remove <z-user> which is horribly confusing,
52     # so we need more of the sentence.
53     r"<z-user></z-user> will have the same role",
54     # Things using "I"
55     r"I understand",
56     r"I'm",
57     r"I've",
58     # Specific short words
59     r"beta",
60     r"and",
61     r"bot",
62     r"e\.g\.",
63     r"enabled",
64     r"signups",
65     # Placeholders
66     r"keyword",
67     r"streamname",
68     r"user@example\.com",
69     # Fragments of larger strings
70     (r"your subscriptions on your Streams page"),
71     r"Add global time<br />Everyone sees global times in their own time zone\.",
72     r"user",
73     r"an unknown operating system",
74     r"Go to Settings",
75     # SPECIAL CASES
76     # Because topics usually are lower-case, this would look weird if it were capitalized
77     r"more topics",
78     # Used alone in a parenthetical where capitalized looks worse.
79     r"^deprecated$",
80     # Capital 'i' looks weird in reminders popover
81     r"in 1 hour",
82     r"in 20 minutes",
83     r"in 3 hours",
84     # these are used as topics
85     r"^new streams$",
86     r"^stream events$",
87     # These are used as example short names (e.g. an uncapitalized context):
88     r"^marketing$",
89     r"^cookie$",
90     # Used to refer custom time limits
91     r"\bN\b",
92     # Capital c feels obtrusive in clear status option
93     r"clear",
94     r"group private messages with \{recipient\}",
95     r"private messages with \{recipient\}",
96     r"private messages with yourself",
97     r"GIF",
98     # Emoji name placeholder
99     r"leafy green vegetable",
100     # Subdomain placeholder
101     r"your-organization-url",
102     # Used in invite modal
103     r"or",
104     # Used in GIPHY popover.
105     r"GIFs",
106     r"GIPHY",
107     # Used in our case studies
108     r"Technical University of Munich",
109     r"University of California San Diego",
110     # Used in stream creation form
111     r"email hidden",
112     # Use in compose box.
113     r"to send",
114     r"to add a new line",
115     # Used in showing Notification Bot read receipts message
116     "Notification Bot",
117     # Used in presence_enabled setting label
118     r"invisible mode off",
119     # Typeahead suggestions for "Pronouns" custom field type.
120     r"he/him",
121     r"she/her",
122     r"they/them",
123 ]
124 
125 # Sort regexes in descending order of their lengths. As a result, the
126 # longer phrases will be ignored first.
127 IGNORED_PHRASES.sort(key=lambda regex: len(regex), reverse=True)
128 
129 # Compile regexes to improve performance. This also extracts the
130 # text using BeautifulSoup and then removes extra whitespaces from
131 # it. This step enables us to add HTML in our regexes directly.
132 COMPILED_IGNORED_PHRASES = [
133     re.compile(" ".join(BeautifulSoup(regex, "lxml").text.split())) for regex in IGNORED_PHRASES
134 ]
135 
136 SPLIT_BOUNDARY = "?.!"  # Used to split string into sentences.
137 SPLIT_BOUNDARY_REGEX = re.compile(rf"[{SPLIT_BOUNDARY}]")
138 
139 # Regexes which check capitalization in sentences.
140 DISALLOWED = [
141     r"^[a-z](?!\})",  # Checks if the sentence starts with a lower case character.
142     r"^[A-Z][a-z]+[\sa-z0-9]+[A-Z]",  # Checks if an upper case character exists
143     # after a lower case character when the first character is in upper case.
144 ]
145 DISALLOWED_REGEX = re.compile(r"|".join(DISALLOWED))
146 
147 BANNED_WORDS = {
148     "realm": "The term realm should not appear in user-facing strings. Use organization instead.",
149 }
150 
151 
152 def get_safe_phrase(phrase: str) -> str:
153     """
154     Safe phrase is in lower case and doesn't contain characters which can
155     conflict with split boundaries. All conflicting characters are replaced
156     with low dash (_).
157     """
158     phrase = SPLIT_BOUNDARY_REGEX.sub("_", phrase)
159     return phrase.lower()
160 
161 
162 def replace_with_safe_phrase(matchobj: Match[str]) -> str:
163     """
164     The idea is to convert IGNORED_PHRASES into safe phrases, see
165     `get_safe_phrase()` function. The only exception is when the
166     IGNORED_PHRASE is at the start of the text or after a split
167     boundary; in this case, we change the first letter of the phrase
168     to upper case.
169     """
170     ignored_phrase = matchobj.group(0)
171     safe_string = get_safe_phrase(ignored_phrase)
172 
173     start_index = matchobj.start()
174     complete_string = matchobj.string
175 
176     is_string_start = start_index == 0
177     # We expect that there will be one space between split boundary
178     # and the next word.
179     punctuation = complete_string[max(start_index - 2, 0)]
180     is_after_split_boundary = punctuation in SPLIT_BOUNDARY
181     if is_string_start or is_after_split_boundary:
182         return safe_string.capitalize()
183 
184     return safe_string
185 
186 
187 def get_safe_text(text: str) -> str:
188     """
189     This returns text which is rendered by BeautifulSoup and is in the
190     form that can be split easily and has all IGNORED_PHRASES processed.
191     """
192     soup = BeautifulSoup(text, "lxml")
193     text = " ".join(soup.text.split())  # Remove extra whitespaces.
194     for phrase_regex in COMPILED_IGNORED_PHRASES:
195         text = phrase_regex.sub(replace_with_safe_phrase, text)
196 
197     return text
198 
199 
200 def is_capitalized(safe_text: str) -> bool:
201     sentences = SPLIT_BOUNDARY_REGEX.split(safe_text)
202     return not any(DISALLOWED_REGEX.search(sentence.strip()) for sentence in sentences)
203 
204 
205 def check_banned_words(text: str) -> List[str]:
206     lower_cased_text = text.lower()
207     errors = []
208     for word, reason in BANNED_WORDS.items():
209         if word in lower_cased_text:
210             # Hack: Should move this into BANNED_WORDS framework; for
211             # now, just hand-code the skips:
212             if "realm_name" in lower_cased_text:
213                 continue
214             kwargs = dict(word=word, text=text, reason=reason)
215             msg = "{word} found in '{text}'. {reason}".format(**kwargs)
216             errors.append(msg)
217 
218     return errors
219 
220 
221 def check_capitalization(strings: List[str]) -> Tuple[List[str], List[str], List[str]]:
222     errors = []
223     ignored = []
224     banned_word_errors = []
225     for text in strings:
226         text = " ".join(text.split())  # Remove extra whitespaces.
227         safe_text = get_safe_text(text)
228         has_ignored_phrase = text != safe_text
229         capitalized = is_capitalized(safe_text)
230         if not capitalized:
231             errors.append(text)
232         elif has_ignored_phrase:
233             ignored.append(text)
234 
235         banned_word_errors.extend(check_banned_words(text))
236 
237     return sorted(errors), sorted(ignored), sorted(banned_word_errors)
238
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/tools/lib/capitalization.py b/tools/lib/capitalization.py
--- a/tools/lib/capitalization.py
+++ b/tools/lib/capitalization.py
@@ -42,6 +42,7 @@
     r"Zephyr",
     r"Zoom",
     r"Zulip",
+    r"Zulip Server",
     r"Zulip Account Security",
     r"Zulip Security",
     r"Zulip Cloud Standard",

{"golden_diff": "diff --git a/tools/lib/capitalization.py b/tools/lib/capitalization.py\n--- a/tools/lib/capitalization.py\n+++ b/tools/lib/capitalization.py\n@@ -42,6 +42,7 @@\n     r\"Zephyr\",\n     r\"Zoom\",\n     r\"Zulip\",\n+    r\"Zulip Server\",\n     r\"Zulip Account Security\",\n     r\"Zulip Security\",\n     r\"Zulip Cloud Standard\",\n", "issue": "Show info about organization and Zulip in gear menu\nAt present, it requires some digging to find a few key pieces of information about the Zulip organization one is viewing:\r\n\r\n- Organization name\r\n- Organization URL (if using the Desktop app)\r\n- For Zulip Cloud, the plan that the organization on.\r\n- For self-hosted Zulip, does the server need to be upgraded?  What's the version of the server, and what's the current release version?\r\n\r\nWe should address this by adding this information at the top of the gear settings menu.\r\n\r\n- For all users: Show organization name and URL\r\n- For all Zulip Cloud users: Show plan name with a link to `/plans`, e.g. \"Zulip Cloud Free\"\r\n- For owners of Zulip Cloud Free orgs: Show \"Upgrade to Zulip Cloud Standard\" link to `/upgrade`\r\n- For all self-hosted users:\r\n   - Show Zulip server version (same as in the \"About Zulip\" widget); we'll need to test to make sure it looks reasonable for non-standard versions (e.g. forks, installs running off `main`).\r\n   - If the server version is old, we should show an \"Upgrade to the latest release (x.y)\" linking to https://zulip.readthedocs.io/en/stable/production/upgrade-or-modify.html. We should probably show this link to all users, as server admins might not be owners/admins of the organization.\r\n\r\n## Mockups\r\n<img width=\"1552\" alt=\"popover-menu\" src=\"https://user-images.githubusercontent.com/2090066/172440944-5dc8ee48-908f-4642-beb7-9ec141128a29.png\">\r\n\r\n<img width=\"1552\" alt=\"dark-inbox-01\" src=\"https://user-images.githubusercontent.com/2090066/172440973-12639e2a-3f42-408d-b976-27b01498ecda.png\">\r\n\r\n<img width=\"1608\" alt=\"selfhosted-upgrade\" src=\"https://user-images.githubusercontent.com/2090066/172441028-c0ce417f-e3db-4542-845f-10ba3fab98df.png\">\r\n\r\n**CZO discussion threads:**\r\n- [Design proposal (Zulip Cloud)](https://chat.zulip.org/#narrow/stream/101-design/topic/UI.20redesign.3A.20popover.20menu/near/1388585)\r\n- [Server upgrade notice](https://chat.zulip.org/#narrow/stream/101-design/topic/server.20upgrade.20notice)\r\n\r\n\n", "before_files": [{"content": "import re\nfrom typing import List, Match, Tuple\n\nfrom bs4 import BeautifulSoup\n\n# The phrases in this list will be ignored. The longest phrase is\n# tried first; this removes the chance of smaller phrases changing\n# the text before longer phrases are tried.\n# The errors shown by `tools/check-capitalization` can be added to\n# this list without any modification.\nIGNORED_PHRASES = [\n    # Proper nouns and acronyms\n    r\"API\",\n    r\"APNS\",\n    r\"Botserver\",\n    r\"Cookie Bot\",\n    r\"DevAuthBackend\",\n    r\"GCM\",\n    r\"GitHub\",\n    r\"Gravatar\",\n    r\"Help Center\",\n    r\"HTTP\",\n    r\"ID\",\n    r\"IDs\",\n    r\"IP\",\n    r\"JSON\",\n    r\"Kerberos\",\n    r\"LDAP\",\n    r\"Markdown\",\n    r\"OTP\",\n    r\"Pivotal\",\n    r\"PM\",\n    r\"PMs\",\n    r\"Slack\",\n    r\"Google\",\n    r\"Terms of Service\",\n    r\"Tuesday\",\n    r\"URL\",\n    r\"UUID\",\n    r\"Webathena\",\n    r\"WordPress\",\n    r\"Zephyr\",\n    r\"Zoom\",\n    r\"Zulip\",\n    r\"Zulip Account Security\",\n    r\"Zulip Security\",\n    r\"Zulip Cloud Standard\",\n    r\"BigBlueButton\",\n    # Code things\n    r\"\\.zuliprc\",\n    # BeautifulSoup will remove <z-user> which is horribly confusing,\n    # so we need more of the sentence.\n    r\"<z-user></z-user> will have the same role\",\n    # Things using \"I\"\n    r\"I understand\",\n    r\"I'm\",\n    r\"I've\",\n    # Specific short words\n    r\"beta\",\n    r\"and\",\n    r\"bot\",\n    r\"e\\.g\\.\",\n    r\"enabled\",\n    r\"signups\",\n    # Placeholders\n    r\"keyword\",\n    r\"streamname\",\n    r\"user@example\\.com\",\n    # Fragments of larger strings\n    (r\"your subscriptions on your Streams page\"),\n    r\"Add global time<br />Everyone sees global times in their own time zone\\.\",\n    r\"user\",\n    r\"an unknown operating system\",\n    r\"Go to Settings\",\n    # SPECIAL CASES\n    # Because topics usually are lower-case, this would look weird if it were capitalized\n    r\"more topics\",\n    # Used alone in a parenthetical where capitalized looks worse.\n    r\"^deprecated$\",\n    # Capital 'i' looks weird in reminders popover\n    r\"in 1 hour\",\n    r\"in 20 minutes\",\n    r\"in 3 hours\",\n    # these are used as topics\n    r\"^new streams$\",\n    r\"^stream events$\",\n    # These are used as example short names (e.g. an uncapitalized context):\n    r\"^marketing$\",\n    r\"^cookie$\",\n    # Used to refer custom time limits\n    r\"\\bN\\b\",\n    # Capital c feels obtrusive in clear status option\n    r\"clear\",\n    r\"group private messages with \\{recipient\\}\",\n    r\"private messages with \\{recipient\\}\",\n    r\"private messages with yourself\",\n    r\"GIF\",\n    # Emoji name placeholder\n    r\"leafy green vegetable\",\n    # Subdomain placeholder\n    r\"your-organization-url\",\n    # Used in invite modal\n    r\"or\",\n    # Used in GIPHY popover.\n    r\"GIFs\",\n    r\"GIPHY\",\n    # Used in our case studies\n    r\"Technical University of Munich\",\n    r\"University of California San Diego\",\n    # Used in stream creation form\n    r\"email hidden\",\n    # Use in compose box.\n    r\"to send\",\n    r\"to add a new line\",\n    # Used in showing Notification Bot read receipts message\n    \"Notification Bot\",\n    # Used in presence_enabled setting label\n    r\"invisible mode off\",\n    # Typeahead suggestions for \"Pronouns\" custom field type.\n    r\"he/him\",\n    r\"she/her\",\n    r\"they/them\",\n]\n\n# Sort regexes in descending order of their lengths. As a result, the\n# longer phrases will be ignored first.\nIGNORED_PHRASES.sort(key=lambda regex: len(regex), reverse=True)\n\n# Compile regexes to improve performance. This also extracts the\n# text using BeautifulSoup and then removes extra whitespaces from\n# it. This step enables us to add HTML in our regexes directly.\nCOMPILED_IGNORED_PHRASES = [\n    re.compile(\" \".join(BeautifulSoup(regex, \"lxml\").text.split())) for regex in IGNORED_PHRASES\n]\n\nSPLIT_BOUNDARY = \"?.!\"  # Used to split string into sentences.\nSPLIT_BOUNDARY_REGEX = re.compile(rf\"[{SPLIT_BOUNDARY}]\")\n\n# Regexes which check capitalization in sentences.\nDISALLOWED = [\n    r\"^[a-z](?!\\})\",  # Checks if the sentence starts with a lower case character.\n    r\"^[A-Z][a-z]+[\\sa-z0-9]+[A-Z]\",  # Checks if an upper case character exists\n    # after a lower case character when the first character is in upper case.\n]\nDISALLOWED_REGEX = re.compile(r\"|\".join(DISALLOWED))\n\nBANNED_WORDS = {\n    \"realm\": \"The term realm should not appear in user-facing strings. Use organization instead.\",\n}\n\n\ndef get_safe_phrase(phrase: str) -> str:\n    \"\"\"\n    Safe phrase is in lower case and doesn't contain characters which can\n    conflict with split boundaries. All conflicting characters are replaced\n    with low dash (_).\n    \"\"\"\n    phrase = SPLIT_BOUNDARY_REGEX.sub(\"_\", phrase)\n    return phrase.lower()\n\n\ndef replace_with_safe_phrase(matchobj: Match[str]) -> str:\n    \"\"\"\n    The idea is to convert IGNORED_PHRASES into safe phrases, see\n    `get_safe_phrase()` function. The only exception is when the\n    IGNORED_PHRASE is at the start of the text or after a split\n    boundary; in this case, we change the first letter of the phrase\n    to upper case.\n    \"\"\"\n    ignored_phrase = matchobj.group(0)\n    safe_string = get_safe_phrase(ignored_phrase)\n\n    start_index = matchobj.start()\n    complete_string = matchobj.string\n\n    is_string_start = start_index == 0\n    # We expect that there will be one space between split boundary\n    # and the next word.\n    punctuation = complete_string[max(start_index - 2, 0)]\n    is_after_split_boundary = punctuation in SPLIT_BOUNDARY\n    if is_string_start or is_after_split_boundary:\n        return safe_string.capitalize()\n\n    return safe_string\n\n\ndef get_safe_text(text: str) -> str:\n    \"\"\"\n    This returns text which is rendered by BeautifulSoup and is in the\n    form that can be split easily and has all IGNORED_PHRASES processed.\n    \"\"\"\n    soup = BeautifulSoup(text, \"lxml\")\n    text = \" \".join(soup.text.split())  # Remove extra whitespaces.\n    for phrase_regex in COMPILED_IGNORED_PHRASES:\n        text = phrase_regex.sub(replace_with_safe_phrase, text)\n\n    return text\n\n\ndef is_capitalized(safe_text: str) -> bool:\n    sentences = SPLIT_BOUNDARY_REGEX.split(safe_text)\n    return not any(DISALLOWED_REGEX.search(sentence.strip()) for sentence in sentences)\n\n\ndef check_banned_words(text: str) -> List[str]:\n    lower_cased_text = text.lower()\n    errors = []\n    for word, reason in BANNED_WORDS.items():\n        if word in lower_cased_text:\n            # Hack: Should move this into BANNED_WORDS framework; for\n            # now, just hand-code the skips:\n            if \"realm_name\" in lower_cased_text:\n                continue\n            kwargs = dict(word=word, text=text, reason=reason)\n            msg = \"{word} found in '{text}'. {reason}\".format(**kwargs)\n            errors.append(msg)\n\n    return errors\n\n\ndef check_capitalization(strings: List[str]) -> Tuple[List[str], List[str], List[str]]:\n    errors = []\n    ignored = []\n    banned_word_errors = []\n    for text in strings:\n        text = \" \".join(text.split())  # Remove extra whitespaces.\n        safe_text = get_safe_text(text)\n        has_ignored_phrase = text != safe_text\n        capitalized = is_capitalized(safe_text)\n        if not capitalized:\n            errors.append(text)\n        elif has_ignored_phrase:\n            ignored.append(text)\n\n        banned_word_errors.extend(check_banned_words(text))\n\n    return sorted(errors), sorted(ignored), sorted(banned_word_errors)\n", "path": "tools/lib/capitalization.py"}], "after_files": [{"content": "import re\nfrom typing import List, Match, Tuple\n\nfrom bs4 import BeautifulSoup\n\n# The phrases in this list will be ignored. The longest phrase is\n# tried first; this removes the chance of smaller phrases changing\n# the text before longer phrases are tried.\n# The errors shown by `tools/check-capitalization` can be added to\n# this list without any modification.\nIGNORED_PHRASES = [\n    # Proper nouns and acronyms\n    r\"API\",\n    r\"APNS\",\n    r\"Botserver\",\n    r\"Cookie Bot\",\n    r\"DevAuthBackend\",\n    r\"GCM\",\n    r\"GitHub\",\n    r\"Gravatar\",\n    r\"Help Center\",\n    r\"HTTP\",\n    r\"ID\",\n    r\"IDs\",\n    r\"IP\",\n    r\"JSON\",\n    r\"Kerberos\",\n    r\"LDAP\",\n    r\"Markdown\",\n    r\"OTP\",\n    r\"Pivotal\",\n    r\"PM\",\n    r\"PMs\",\n    r\"Slack\",\n    r\"Google\",\n    r\"Terms of Service\",\n    r\"Tuesday\",\n    r\"URL\",\n    r\"UUID\",\n    r\"Webathena\",\n    r\"WordPress\",\n    r\"Zephyr\",\n    r\"Zoom\",\n    r\"Zulip\",\n    r\"Zulip Server\",\n    r\"Zulip Account Security\",\n    r\"Zulip Security\",\n    r\"Zulip Cloud Standard\",\n    r\"BigBlueButton\",\n    # Code things\n    r\"\\.zuliprc\",\n    # BeautifulSoup will remove <z-user> which is horribly confusing,\n    # so we need more of the sentence.\n    r\"<z-user></z-user> will have the same role\",\n    # Things using \"I\"\n    r\"I understand\",\n    r\"I'm\",\n    r\"I've\",\n    # Specific short words\n    r\"beta\",\n    r\"and\",\n    r\"bot\",\n    r\"e\\.g\\.\",\n    r\"enabled\",\n    r\"signups\",\n    # Placeholders\n    r\"keyword\",\n    r\"streamname\",\n    r\"user@example\\.com\",\n    # Fragments of larger strings\n    (r\"your subscriptions on your Streams page\"),\n    r\"Add global time<br />Everyone sees global times in their own time zone\\.\",\n    r\"user\",\n    r\"an unknown operating system\",\n    r\"Go to Settings\",\n    # SPECIAL CASES\n    # Because topics usually are lower-case, this would look weird if it were capitalized\n    r\"more topics\",\n    # Used alone in a parenthetical where capitalized looks worse.\n    r\"^deprecated$\",\n    # Capital 'i' looks weird in reminders popover\n    r\"in 1 hour\",\n    r\"in 20 minutes\",\n    r\"in 3 hours\",\n    # these are used as topics\n    r\"^new streams$\",\n    r\"^stream events$\",\n    # These are used as example short names (e.g. an uncapitalized context):\n    r\"^marketing$\",\n    r\"^cookie$\",\n    # Used to refer custom time limits\n    r\"\\bN\\b\",\n    # Capital c feels obtrusive in clear status option\n    r\"clear\",\n    r\"group private messages with \\{recipient\\}\",\n    r\"private messages with \\{recipient\\}\",\n    r\"private messages with yourself\",\n    r\"GIF\",\n    # Emoji name placeholder\n    r\"leafy green vegetable\",\n    # Subdomain placeholder\n    r\"your-organization-url\",\n    # Used in invite modal\n    r\"or\",\n    # Used in GIPHY popover.\n    r\"GIFs\",\n    r\"GIPHY\",\n    # Used in our case studies\n    r\"Technical University of Munich\",\n    r\"University of California San Diego\",\n    # Used in stream creation form\n    r\"email hidden\",\n    # Use in compose box.\n    r\"to send\",\n    r\"to add a new line\",\n    # Used in showing Notification Bot read receipts message\n    \"Notification Bot\",\n    # Used in presence_enabled setting label\n    r\"invisible mode off\",\n    # Typeahead suggestions for \"Pronouns\" custom field type.\n    r\"he/him\",\n    r\"she/her\",\n    r\"they/them\",\n]\n\n# Sort regexes in descending order of their lengths. As a result, the\n# longer phrases will be ignored first.\nIGNORED_PHRASES.sort(key=lambda regex: len(regex), reverse=True)\n\n# Compile regexes to improve performance. This also extracts the\n# text using BeautifulSoup and then removes extra whitespaces from\n# it. This step enables us to add HTML in our regexes directly.\nCOMPILED_IGNORED_PHRASES = [\n    re.compile(\" \".join(BeautifulSoup(regex, \"lxml\").text.split())) for regex in IGNORED_PHRASES\n]\n\nSPLIT_BOUNDARY = \"?.!\"  # Used to split string into sentences.\nSPLIT_BOUNDARY_REGEX = re.compile(rf\"[{SPLIT_BOUNDARY}]\")\n\n# Regexes which check capitalization in sentences.\nDISALLOWED = [\n    r\"^[a-z](?!\\})\",  # Checks if the sentence starts with a lower case character.\n    r\"^[A-Z][a-z]+[\\sa-z0-9]+[A-Z]\",  # Checks if an upper case character exists\n    # after a lower case character when the first character is in upper case.\n]\nDISALLOWED_REGEX = re.compile(r\"|\".join(DISALLOWED))\n\nBANNED_WORDS = {\n    \"realm\": \"The term realm should not appear in user-facing strings. Use organization instead.\",\n}\n\n\ndef get_safe_phrase(phrase: str) -> str:\n    \"\"\"\n    Safe phrase is in lower case and doesn't contain characters which can\n    conflict with split boundaries. All conflicting characters are replaced\n    with low dash (_).\n    \"\"\"\n    phrase = SPLIT_BOUNDARY_REGEX.sub(\"_\", phrase)\n    return phrase.lower()\n\n\ndef replace_with_safe_phrase(matchobj: Match[str]) -> str:\n    \"\"\"\n    The idea is to convert IGNORED_PHRASES into safe phrases, see\n    `get_safe_phrase()` function. The only exception is when the\n    IGNORED_PHRASE is at the start of the text or after a split\n    boundary; in this case, we change the first letter of the phrase\n    to upper case.\n    \"\"\"\n    ignored_phrase = matchobj.group(0)\n    safe_string = get_safe_phrase(ignored_phrase)\n\n    start_index = matchobj.start()\n    complete_string = matchobj.string\n\n    is_string_start = start_index == 0\n    # We expect that there will be one space between split boundary\n    # and the next word.\n    punctuation = complete_string[max(start_index - 2, 0)]\n    is_after_split_boundary = punctuation in SPLIT_BOUNDARY\n    if is_string_start or is_after_split_boundary:\n        return safe_string.capitalize()\n\n    return safe_string\n\n\ndef get_safe_text(text: str) -> str:\n    \"\"\"\n    This returns text which is rendered by BeautifulSoup and is in the\n    form that can be split easily and has all IGNORED_PHRASES processed.\n    \"\"\"\n    soup = BeautifulSoup(text, \"lxml\")\n    text = \" \".join(soup.text.split())  # Remove extra whitespaces.\n    for phrase_regex in COMPILED_IGNORED_PHRASES:\n        text = phrase_regex.sub(replace_with_safe_phrase, text)\n\n    return text\n\n\ndef is_capitalized(safe_text: str) -> bool:\n    sentences = SPLIT_BOUNDARY_REGEX.split(safe_text)\n    return not any(DISALLOWED_REGEX.search(sentence.strip()) for sentence in sentences)\n\n\ndef check_banned_words(text: str) -> List[str]:\n    lower_cased_text = text.lower()\n    errors = []\n    for word, reason in BANNED_WORDS.items():\n        if word in lower_cased_text:\n            # Hack: Should move this into BANNED_WORDS framework; for\n            # now, just hand-code the skips:\n            if \"realm_name\" in lower_cased_text:\n                continue\n            kwargs = dict(word=word, text=text, reason=reason)\n            msg = \"{word} found in '{text}'. {reason}\".format(**kwargs)\n            errors.append(msg)\n\n    return errors\n\n\ndef check_capitalization(strings: List[str]) -> Tuple[List[str], List[str], List[str]]:\n    errors = []\n    ignored = []\n    banned_word_errors = []\n    for text in strings:\n        text = \" \".join(text.split())  # Remove extra whitespaces.\n        safe_text = get_safe_text(text)\n        has_ignored_phrase = text != safe_text\n        capitalized = is_capitalized(safe_text)\n        if not capitalized:\n            errors.append(text)\n        elif has_ignored_phrase:\n            ignored.append(text)\n\n        banned_word_errors.extend(check_banned_words(text))\n\n    return sorted(errors), sorted(ignored), sorted(banned_word_errors)\n", "path": "tools/lib/capitalization.py"}]}