i18n: Add scripts for web app legacy "stream" string translations.

Adds tools/i18n/create-legacy-stream-translations to create a
`legacy_stream_translations.json` file for every non-English
language locale that will serve as the legacy translations for
the stream to channel rename.

Adds tools/i18n/update-for-legacy-translations to manage adding
the legacy translations for any stream/channel strings that we'd
like to maintain existing translations for, which are defined in
LEGACY_STRINGS_MAP.
This commit is contained in:
Lauryn Menard 2024-05-01 13:27:19 +02:00 committed by Tim Abbott
parent 1d73a85a92
commit d651441f66
3 changed files with 288 additions and 0 deletions

View File

@ -0,0 +1,44 @@
#!/usr/bin/env python3
import json
import os
import re
from subprocess import check_output
from typing import Dict, List
def get_json_filename(locale: str) -> str:
return f"locale/{locale}/translations.json"
def get_locales() -> List[str]:
output = check_output(["git", "ls-files", "locale"], text=True)
tracked_files = output.split()
regex = re.compile(r"locale/(\w+)/LC_MESSAGES/django.po")
locales = []
for tracked_file in tracked_files:
matched = regex.search(tracked_file)
if matched and matched.group(1) != "en_GB":
locales.append(matched.group(1))
return locales
def create_legacy_stream_translations(resource: str) -> None:
with open(resource) as raw_resource_file:
translated_strings = json.load(raw_resource_file)
stream_strings: Dict[str, str] = {}
for line in translated_strings:
if "stream" in str(line).lower() and translated_strings[line] != "":
stream_strings[line] = translated_strings[line]
legacy_path = os.path.join("locale", locale, "legacy_stream_translations.json")
with open(legacy_path, "w") as f:
json.dump(stream_strings, f, ensure_ascii=False, indent=2, sort_keys=True)
f.write("\n")
for locale in get_locales():
path = get_json_filename(locale)
if os.path.exists(path):
create_legacy_stream_translations(path)

View File

@ -21,5 +21,7 @@ for file in "${files[@]}"; do
uconv -x any-nfc "$file" | sponge -- "$file"
done
./tools/i18n/update-for-legacy-translations
./manage.py compilemessages --ignore='*'
./tools/i18n/process-mobile-i18n

View File

@ -0,0 +1,242 @@
#!/usr/bin/env python3
import json
import os
import re
from subprocess import check_output
from typing import Dict, List
LEGACY_STRINGS_MAP = {
"<p>You are searching for messages that belong to more than one channel, which is not possible.</p>": "<p>You are searching for messages that belong to more than one stream, which is not possible.</p>",
"<strong>{name}</strong> <i>(guest)</i> is not subscribed to this channel. They will not be notified if you mention them.": "<strong>{name}</strong> <i>(guest)</i> is not subscribed to this stream. They will not be notified if you mention them.",
"<strong>{name}</strong> <i>(guest)</i> is not subscribed to this channel. They will not be notified unless you subscribe them.": "<strong>{name}</strong> <i>(guest)</i> is not subscribed to this stream. They will not be notified unless you subscribe them.",
"<strong>{name}</strong> is not subscribed to this channel. They will not be notified if you mention them.": "<strong>{name}</strong> is not subscribed to this stream. They will not be notified if you mention them.",
"<strong>{name}</strong> is not subscribed to this channel. They will not be notified unless you subscribe them.": "<strong>{name}</strong> is not subscribed to this stream. They will not be notified unless you subscribe them.",
"<z-link>Click here</z-link> to learn about exporting private channels and direct messages.": "<z-link>Click here</z-link> to learn about exporting private streams and direct messages.",
"<z-user></z-user> will have the same properties as it did prior to deactivation, including role, owner and channel subscriptions.": "<z-user></z-user> will have the same properties as it did prior to deactivation, including role, owner and stream subscriptions.",
"<z-user></z-user> will have the same role, channel subscriptions, user group memberships, and other settings and permissions as they did prior to deactivation.": "<z-user></z-user> will have the same role, stream subscriptions, user group memberships, and other settings and permissions as they did prior to deactivation.",
"A channel with this name already exists.": "A stream with this name already exists.",
"Add default channels": "Add default streams",
"Add members. Use usergroup or #channelname to bulk add members.": "Add members. Use usergroup or #streamname to bulk add members.",
"Add channel": "Add stream",
"Add channels": "Add streams",
"Add subscribers. Use usergroup or #channelname to bulk add subscribers.": "Add subscribers. Use usergroup or #streamname to bulk add subscribers.",
"All messages including muted channels": "All messages including muted streams",
"All channels": "All streams",
"Allow creating web-public streams (visible to anyone on the Internet)": "Allow creating web-public streams (visible to anyone on the Internet)",
# "Already subscribed to {channel}": "Already subscribed to {stream}",
"Announce new channel in": "Announce new stream in",
"Archive channel": "Archive stream",
"Archiving channel <z-stream></z-stream> will immediately unsubscribe everyone. This action cannot be undone.": "Archiving stream <z-stream></z-stream> will immediately unsubscribe everyone. This action cannot be undone.",
"Archiving this channel will also disable settings that were configured to use this channel:": "Archiving this stream will also disable settings that were configured to use this stream:",
# "Are you sure you want to create channel ''''{channel_name}'''' and subscribe {count} users to it?": "Are you sure you want to create stream ''''{stream_name}'''' and subscribe {count} users to it?",
# "Are you sure you want to send @-mention notifications to the <strong>{subscriber_count}</strong> users subscribed to #{channel_name}? If not, please edit your message to remove the <strong>@{wildcard_mention}</strong> mention.": "Are you sure you want to send @-mention notifications to the <strong>{subscriber_count}</strong> users subscribed to #{stream_name}? If not, please edit your message to remove the <strong>@{stream_wildcard_mention}</strong> mention.",
"Automatically unmute topics in muted channels": "Automatically unmute topics in muted streams",
"Back to channels": "Back to streams",
"Because you are removing the last subscriber from a private channel, it will be automatically <z-link>archived</z-link>.": "Because you are removing the last subscriber from a private stream, it will be automatically <z-link>archived</z-link>.",
"Because you are the only subscriber, this channel will be automatically <z-link>archived</z-link>.": "Because you are the only subscriber, this stream will be automatically <z-link>archived</z-link>.",
"Browse 1 more channel": "Browse 1 more stream",
"Browse channels": "Browse streams",
"Browse {can_subscribe_stream_count} more channels": "Browse {can_subscribe_stream_count} more streams",
"Cannot subscribe to private channel <z-stream></z-stream>": "Cannot subscribe to private stream <z-stream></z-stream>",
"Cannot view channel": "Cannot view stream",
"Choose a name for the new channel.": "Choose a name for the new stream.",
"Configure how Zulip notifies you about new messages. In muted channels, channel notification settings apply only to unmuted topics.": "Configure how Zulip notifies you about new messages. In muted streams, stream notification settings apply only to unmuted topics.",
"Configure the default channels new users are subscribed to when joining your organization.": "Configure the default streams new users are subscribed to when joining your organization.",
"Consider <z-link>searching all public channels</z-link>.": "Consider <z-link>searching all public streams</z-link>.",
"Create a channel": "Create a stream",
"Create new channel": "Create new stream",
"Create channel": "Create stream",
"Creating channel...": "Creating stream...",
"Currently viewing the entire channel.": "Currently viewing the entire stream.",
"Cycle between channel views": "Cycle between stream views",
"Default for channel": "Default for stream",
"Default channels": "Default streams",
"Default channels for new users cannot be made private.": "Default streams for new users cannot be made private.",
"Default channels for this organization": "Default streams for this organization",
"Demote inactive channels": "Demote inactive streams",
# "Edit #{channel_name}": "Edit #{stream_name}",
"Edit channel name and description": "Edit stream name and description",
"Error creating channel": "Error creating stream",
# "Error in unsubscribing from #{channel_name}": "Error in unsubscribing from #{stream_name}",
# "Error removing user from #{channel_name}": "Error removing user from #{stream_name}",
"Error removing user from this channel.": "Error removing user from this stream.",
"Exports all users, settings, and all data visible in public channels.": "Exports all users, settings, and all data visible in public streams.",
"Failed adding one or more channels.": "Failed adding one or more streams.",
"Filter default channels": "Filter default streams",
"Filter channels": "Filter streams",
"First time? Read our <z-link>guidelines</z-link> for creating and naming channels.": "First time? Read our <z-link>guidelines</z-link> for creating and naming streams.",
"Generate channel email address": "Generate stream email address",
"Go to channel from topic view": "Go to stream from topic view",
"Go to channel settings": "Go to stream settings",
"However, it will no longer be subscribed to the private channels that you are not subscribed to.": "However, it will no longer be subscribed to the private streams that you are not subscribed to.",
"In muted channels, channel notification settings apply only to unmuted topics.": "In muted streams, stream notification settings apply only to unmuted topics.",
"In this channel": "In this stream",
"Includes muted channels and topics": "Includes muted streams and topics",
"Invalid channel ID": "Invalid stream ID",
"Let recipients see when I'm typing messages in channels": "Let recipients see when I'm typing messages in streams",
"Let recipients see when a user is typing channel messages": "Let recipients see when a user is typing stream messages",
"Log in to browse more channels": "Log in to browse more streams",
# "Message #{channel_name}": "Message #{stream_name}",
# "Message #{channel_name} > {topic_name}": "Message #{stream_name} > {topic_name}",
"Messages in all public channels": "Messages in all public streams",
"Mute channel": "Mute stream",
"Narrow to messages on channel <z-value></z-value>.": "Narrow to messages on stream <z-value></z-value>.",
"New channel announcements": "New stream announcements",
"New channel message": "New stream message",
"New channel notifications": "New stream notifications",
"No default channels match your current filter.": "No default streams match your current filter.",
"No matching channels": "No matching streams",
"No channel subscribers match your current filter.": "No stream subscribers match your current filter.",
"No channel subscriptions.": "No stream subscriptions.",
"No channels": "No streams",
"Notify channel": "Notify stream",
# "Now following <z-link>{channel_topic}</z-link>.": "Now following <z-link>{stream_topic}</z-link>.",
"Once you leave this channel, you will not be able to rejoin.": "Once you leave this stream, you will not be able to rejoin.",
"Only channel members can add users to a private channel.": "Only stream members can add users to a private stream.",
"Only subscribers can access or join private channels, so you will lose access to this channel if you convert it to a private channel while not subscribed to it.": "Only subscribers can access or join private streams, so you will lose access to this stream if you convert it to a private stream while not subscribed to it.",
"Only subscribers to this channel can edit channel permissions.": "Only subscribers to this stream can edit stream permissions.",
"Pin channel to top": "Pin stream to top",
"Pin channel to top of left sidebar": "Pin stream to top of left sidebar",
"Please specify a channel.": "Please specify a stream.",
"Private channels cannot be default channels for new users.": "Private streams cannot be default streams for new users.",
"Receives new channel announcements": "Receives new stream announcements",
"Require topics in channel messages": "Require topics in stream messages",
"CHANNELS": "STREAMS",
"Scroll through channels": "Scroll through streams",
"Search all public channels in the organization.": "Search all public streams in the organization.",
"Select a channel": "Select a stream",
"Select a channel below or change topic name.": "Select a stream below or change topic name.",
"Select a channel to subscribe": "Select a stream to subscribe",
"Select channel": "Select stream",
"Channel": "Stream",
"Channel <b><z-stream></z-stream></b> created!": "Stream <b><z-stream></z-stream></b> created!",
"Channel ID": "Stream ID",
"Channel color": "Stream color",
"Channel created recently": "Stream created recently",
"Channel creation": "Stream creation",
"Channel description": "Stream description",
"Channel details": "Stream details",
"Channel email address:": "Stream email address:",
"Channel name": "Stream name",
"Channel permissions": "Stream permissions",
"Channel settings": "Stream settings",
"Channel successfully created!": "Stream successfully created!",
"Channels": "Streams",
"Channels they should join": "Streams they should join",
"Subscribe to/unsubscribe from selected channel": "Subscribe to/unsubscribe from selected stream",
"Subscribe {full_name} to channels": "Subscribe {full_name} to streams",
"Subscribed channels": "Subscribed streams",
# "The channel <b>#{channel_name}</b> does not exist. Manage your subscriptions <z-link>on your Channels page</z-link>.": "The stream <b>#{stream_name}</b> does not exist. Manage your subscriptions <z-link>on your Streams page</z-link>.",
"The channel description cannot contain newline characters.": "The stream description cannot contain newline characters.",
"The topic <strong>{topic_name}</strong> already exists in this channel. Are you sure you want to combine messages from these topics? This cannot be undone.": "The topic <strong>{topic_name}</strong> already exists in this stream. Are you sure you want to combine messages from these topics? This cannot be undone.",
"There are no default channels.": "There are no default streams.",
"There are no channels you can view in this organization.": "There are no streams you can view in this organization.",
"This change will make this channel's entire message history accessible according to the new configuration.": "This change will make this stream's entire message history accessible according to the new configuration.",
"This channel does not exist or is private.": "This stream does not exist or is private.",
"This channel does not yet have a description.": "This stream does not yet have a description.",
"This channel has been archived.": "This stream has been archived.",
"This channel has no subscribers.": "This stream has no subscribers.",
"This channel has {sub_count, plural, =0 {no subscribers} one {# subscriber} other {# subscribers}}.": "This stream has {sub_count, plural, =0 {no subscribers} one {# subscriber} other {# subscribers}}.",
"Time limit for moving messages between channels": "Time limit for moving messages between streams",
"Unknown channel": "Unknown stream",
"Unknown channel #{search_text}": "Unknown stream #{search_text}",
"Unmute channel": "Unmute stream",
# "Unmuted <z-link>{channel_topic}</z-link>.": "Unmuted <z-link>{stream_topic}</z-link>.",
"Unmuted channels and topics": "Unmuted streams and topics",
"Unpin channel from top": "Unpin stream from top",
"Use channel settings to unsubscribe from private channels.": "Use stream settings to unsubscribe from private streams.",
"Use channel settings to unsubscribe the last user from a private channel.": "Use stream settings to unsubscribe the last user from a private stream.",
"View all channels": "View all streams",
"View channel": "View stream",
"View channel messages": "View stream messages",
"View channels": "View streams",
# "Warning: <strong>#{channel_name}</strong> is a private channel.": "Warning: <strong>#{stream_name}</strong> is a private stream.",
"Which parts of the email should be included in the Zulip message sent to this channel?": "Which parts of the email should be included in the Zulip message sent to this stream?",
"Who can access the channel?": "Who can access the stream?",
"Who can add users to channels": "Who can add users to streams",
"Who can create private channels": "Who can create private streams",
"Who can create public channels": "Who can create public streams",
"Who can create web-public channels": "Who can create web-public streams",
"Who can move messages to another channel": "Who can move messages to another stream",
"Who can post to the channel?": "Who can post to the stream?",
"Who can unsubscribe others from this channel?": "Who can unsubscribe others from this stream?",
"You are not currently subscribed to this channel.": "You are not currently subscribed to this stream.",
"You are not subscribed to any channels.": "You are not subscribed to any streams.",
"You are not subscribed to channel <z-stream-name></z-stream-name>.": "You are not subscribed to stream <z-stream-name></z-stream-name>.",
# "You aren't subscribed to this channel and nobody has talked about that yet!": "You aren't subscribed to this stream and nobody has talked about that yet!",
"You can use email to send messages to Zulip channels.": "You can use email to send messages to Zulip streams.",
"You cannot create a channel with no subscribers.": "You cannot create a stream with no subscribers.",
"You do not have permission to add other users to channels in this organization.": "You do not have permission to add other users to streams in this organization.",
"You do not have permission to move messages to another channel in this organization.": "You do not have permission to move messages to another stream in this organization.",
"You do not have permission to post in this channel.": "You do not have permission to post in this stream.",
# "You do not have permission to use <b>@{wildcard_mention_string}</b> mentions in this channel.": "You do not have permission to use <b>@{stream_wildcard_mention}</b> mentions in this stream.",
"You must be an organization administrator to create a channel without subscribing.": "You must be an organization administrator to create a stream without subscribing.",
"You subscribed to channel <z-stream-name></z-stream-name>.": "You subscribed to stream <z-stream-name></z-stream-name>.",
"You unsubscribed from channel <z-stream-name></z-stream-name>.": "You unsubscribed from stream <z-stream-name></z-stream-name>.",
"You're not subscribed to this channel. You will not be notified if other users reply to your message.": "You're not subscribed to this stream. You will not be notified if other users reply to your message.",
"Your message was sent to a channel you have muted.": "Your message was sent to a stream you have muted.",
"back to channels": "back to streams",
}
def get_json_filename(locale: str) -> str:
return f"locale/{locale}/translations.json"
def get_legacy_filename(locale: str) -> str:
return f"locale/{locale}/legacy_stream_translations.json"
def get_locales() -> List[str]:
output = check_output(["git", "ls-files", "locale"], text=True)
tracked_files = output.split()
regex = re.compile(r"locale/(\w+)/LC_MESSAGES/django.po")
locales = []
for tracked_file in tracked_files:
matched = regex.search(tracked_file)
if matched and matched.group(1) != "en_GB":
locales.append(matched.group(1))
return locales
def get_translations(path: str) -> Dict[str, str]:
with open(path) as raw_resource_file:
translations = json.load(raw_resource_file)
return translations
def update_for_legacy_stream_translations(
current: Dict[str, str], legacy: Dict[str, str], path: str
) -> None:
number_of_updates = 0
updated_translations: Dict[str, str] = {}
for line in current:
# If the string has a legacy string mapped and see if it's
# not currently translated (e.g. an empty string), then use
# the legacy translated string (which might be an empty string).
if line in LEGACY_STRINGS_MAP and current[line] == "":
legacy_string = LEGACY_STRINGS_MAP[line]
if legacy_string in legacy:
updated_translations[line] = legacy[legacy_string]
number_of_updates += 1
else:
updated_translations[line] = current[line]
# Only replace file content if we've made any updates for legacy
# translated strings.
if number_of_updates > 0:
with open(path, "w") as f:
json.dump(updated_translations, f, ensure_ascii=False, indent=2, sort_keys=True)
f.write("\n")
print(f"Updated {number_of_updates} strings in: {path}")
for locale in get_locales():
current = get_json_filename(locale)
legacy = get_legacy_filename(locale)
if os.path.exists(current) and os.path.exists(legacy):
current_translations = get_translations(current)
legacy_translations = get_translations(legacy)
update_for_legacy_stream_translations(current_translations, legacy_translations, current)