First Commit
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# Localazy
|
||||
|
||||
Localazy is used to host the source strings and their translations.
|
||||
|
||||
<!--- TOC -->
|
||||
|
||||
* [Localazy project](#localazy-project)
|
||||
* [Key naming rules](#key-naming-rules)
|
||||
* [Special suffixes](#special-suffixes)
|
||||
* [Placeholders](#placeholders)
|
||||
* [CLI Installation](#cli-installation)
|
||||
* [Download translations](#download-translations)
|
||||
* [Add translations to a specific module](#add-translations-to-a-specific-module)
|
||||
|
||||
<!--- END -->
|
||||
|
||||
## Localazy project
|
||||
|
||||
[](https://localazy.com/p/element)
|
||||
|
||||
To add new strings, or to translate existing strings, go the the Localazy project: [https://localazy.com/p/element](https://localazy.com/p/element). Please follow the key naming rules (see below).
|
||||
|
||||
Never edit manually the files `localazy.xml` or `translations.xml`!.
|
||||
|
||||
### Key naming rules
|
||||
|
||||
For code clarity and in order to download strings to the correct module, here are some naming rules to follow as much as possible:
|
||||
|
||||
- Keys for common strings, i.e. strings that can be used at multiple places must start by `action.` if this is a verb, or `common.` if not;
|
||||
- Keys for common accessibility strings must start by `a11y.`. Example: `a11y.hide_password`;
|
||||
- Keys for common strings should be named to match the string. Example: `action.copy_link` for the string `Copy link`;
|
||||
- When creating common strings, make sure to enable "Use dot (.) to create nested keys";
|
||||
- Keys for strings used in a single screen must start with `screen_` followed by the screen name, followed by a free name. Example: `screen_onboarding_welcome_title`;
|
||||
- Keys can have `_title` or `_subtitle` suffixes. Example: `screen_onboarding_welcome_title`, `screen_change_server_subtitle`;
|
||||
- For dialogs, keys can have `_dialog_title`, `_dialog_content`, and `_dialog_submit` suffixes. Example: `screen_signout_confirmation_dialog_title`, `screen_signout_confirmation_dialog_content`, `screen_signout_confirmation_dialog_submit`;
|
||||
- `a11y.` pattern can be used for strings that are only used for accessibility. Example: `a11y.hide_password`, `screen_roomlist_a11y_create_message`;
|
||||
- Strings for error message can start by `error_`, or contain `_error_` if used in a specific screen only. Example: `error_some_messages_have_not_been_sent`, `screen_change_server_error_invalid_homeserver`;
|
||||
|
||||
*Note*: those rules applies for `strings` and for `plurals`.
|
||||
|
||||
#### Special suffixes
|
||||
|
||||
- if a key is suffixed by `_ios`, it will not be imported in the Android project;
|
||||
- if a key is suffixed by `_android`, it will not be imported in the iOS project.
|
||||
|
||||
So feel free to use those suffixes when necessary for instance when the string content is referring to something related to Android only, or iOS only.
|
||||
|
||||
#### Placeholders
|
||||
|
||||
Placeholders should have the form `%1$s`, `%1$d`, etc.. Please use numbered placeholders. Note that Localazy will take care of converting the placeholder to Android (-> `%1$s`) and iOS specific format (-> `%1$@`). Ideally add a comment on Localazy to explain with what the placeholder(s) will be replaced at runtime.
|
||||
|
||||
## CLI Installation
|
||||
|
||||
To install the Localazy client, follow the instructions from [here](https://localazy.com/docs/cli/installation).
|
||||
|
||||
## Download translations
|
||||
|
||||
In the root folder of the project, run:
|
||||
|
||||
```shell
|
||||
./tools/localazy/downloadStrings.sh
|
||||
```
|
||||
|
||||
It will update all the `localazy.xml` resource files. In case of merge conflicts, just erase the files and download again using the script.
|
||||
|
||||
To also include the translations, i.e. the `translations.xml` files, add `--all` argument:
|
||||
|
||||
```shell
|
||||
./tools/localazy/downloadStrings.sh --all
|
||||
```
|
||||
|
||||
## Add translations to a specific module
|
||||
|
||||
Edit the file [config.json](./config.json) to add a new module, or add a new item in `includeRegex` arrays, then run the script again to see the effect.
|
||||
|
||||
[generateLocalazyConfig.py](generateLocalazyConfig.py) is the Python script that convert `config.json` to a localazy configuration file. Generally you should not edit this file.
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import re
|
||||
import sys
|
||||
from xml.dom import minidom
|
||||
|
||||
file = sys.argv[1]
|
||||
|
||||
# Dict of forbidden terms, with exceptions for some String name
|
||||
# Keys are the terms, values are the exceptions.
|
||||
forbiddenTerms = {
|
||||
r"\bElement\b": [
|
||||
# Those 2 strings are only used in debug version
|
||||
"screen_advanced_settings_element_call_base_url",
|
||||
"screen_advanced_settings_element_call_base_url_description",
|
||||
# only used for element.io homeserver, so it's fine
|
||||
"screen_server_confirmation_message_login_element_dot_io",
|
||||
# "Be in your element", will probably be changed on the forks, so we can ignore.
|
||||
"screen_onboarding_welcome_title",
|
||||
# Contains "Element Call"
|
||||
"screen_incoming_call_subtitle_android",
|
||||
"call_invalid_audio_device_bluetooth_devices_disabled",
|
||||
# Contains "Element X"
|
||||
"screen_room_timeline_legacy_call",
|
||||
# We explicitly want to mention Element Pro in these 2:
|
||||
"screen_change_server_error_element_pro_required_title",
|
||||
"screen_change_server_error_element_pro_required_message",
|
||||
]
|
||||
}
|
||||
|
||||
content = minidom.parse(file)
|
||||
|
||||
errors = []
|
||||
|
||||
### Strings
|
||||
for elem in content.getElementsByTagName('string'):
|
||||
name = elem.attributes['name'].value
|
||||
# Continue if value is empty
|
||||
child = elem.firstChild
|
||||
if child is None:
|
||||
# Should not happen
|
||||
continue
|
||||
value = child.nodeValue
|
||||
# If value contains a forbidden term, add the error to errors
|
||||
for (term, exceptions) in forbiddenTerms.items():
|
||||
matches = re.search(term, value)
|
||||
if matches and name not in exceptions:
|
||||
errors.append('Forbidden term "' + term + '" in string: "' + name + '": ' + value)
|
||||
|
||||
### Plurals
|
||||
for elem in content.getElementsByTagName('plurals'):
|
||||
name = elem.attributes['name'].value
|
||||
for it in elem.childNodes:
|
||||
if it.nodeType != it.ELEMENT_NODE:
|
||||
continue
|
||||
# Continue if value is empty
|
||||
child = it.firstChild
|
||||
if child is None:
|
||||
# Should not happen
|
||||
continue
|
||||
value = child.nodeValue
|
||||
# If value contains a forbidden term, add the error to errors
|
||||
for (term, exceptions) in forbiddenTerms.items():
|
||||
matches = re.search(term, value)
|
||||
if matches and name not in exceptions:
|
||||
errors.append('Forbidden term "' + term + '" in plural: "' + name + '": ' + value)
|
||||
|
||||
# If errors is not empty print the report
|
||||
if errors:
|
||||
print('Error(s) in file ' + file + ":", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(" - " + error, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,389 @@
|
||||
{
|
||||
"modules" : [
|
||||
{
|
||||
"name" : ":appnav",
|
||||
"includeRegex" : [
|
||||
"banner\\.migrate_to_native_sliding_sync\\.force_logout.title",
|
||||
"banner\\.migrate_to_native_sliding_sync\\.action",
|
||||
"banner\\.migrate_to_native_sliding_sync\\.app_force_logout\\.title"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:rageshake:impl",
|
||||
"includeRegex" : [
|
||||
"screen_bug_report_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:rageshake:api",
|
||||
"includeRegex" : [
|
||||
"crash_detection_.*",
|
||||
"rageshake_detection_.*",
|
||||
"settings_rageshake.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:announcement:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.space_announcement\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:logout:impl",
|
||||
"includeRegex" : [
|
||||
"screen_signout_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:deactivation:impl",
|
||||
"includeRegex" : [
|
||||
"screen_deactivate_account_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:roomaliasresolver:impl",
|
||||
"includeRegex" : [
|
||||
"screen_room_alias_resolver_.*",
|
||||
"screen.join_room.loading_alert_title"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:signedout:impl",
|
||||
"includeRegex" : [
|
||||
"screen_signed_out_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:invite:impl",
|
||||
"includeRegex" : [
|
||||
"screen_invites_.*",
|
||||
"screen\\.join_room\\.decline_and_block_.*",
|
||||
"screen\\.decline_and_block\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:createroom:impl",
|
||||
"includeRegex" : [
|
||||
"screen_create_room_.*",
|
||||
"screen\\.create_room\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:startchat:impl",
|
||||
"includeRegex" : [
|
||||
"screen_start_chat_.*",
|
||||
"screen\\.start_chat\\..*",
|
||||
"screen_room_directory_search_title",
|
||||
"screen_create_room_action_create_room"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:verifysession:impl",
|
||||
"includeRegex" : [
|
||||
"screen_session_verification_.*",
|
||||
"screen_signout_in_progress_dialog_content",
|
||||
"screen_identity_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:textcomposer:impl",
|
||||
"includeRegex" : [
|
||||
"rich_text_editor.*",
|
||||
".*voice_message_tooltip",
|
||||
"screen\\.media_upload_preview.caption_warning"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:dateformatter:impl",
|
||||
"includeRegex" : [
|
||||
"common\\.date\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:permissions:api",
|
||||
"includeRegex" : [
|
||||
"dialog\\.permission_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:androidutils",
|
||||
"includeRegex" : [
|
||||
"error_no_compatible_app_found"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:mediaviewer:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.media_details\\..*",
|
||||
"screen_media_browser_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:eventformatter:impl",
|
||||
"includeRegex" : [
|
||||
"state_event_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:push:impl",
|
||||
"includeRegex" : [
|
||||
"push_.*",
|
||||
"notification_.*",
|
||||
"notification\\..*",
|
||||
"troubleshoot_notifications\\.test_blocked_users\\..*",
|
||||
"troubleshoot_notifications_test_current_push_provider.*",
|
||||
"troubleshoot_notifications_test_detect_push_provider.*",
|
||||
"troubleshoot_notifications_test_display_notification_.*",
|
||||
"troubleshoot_notifications_test_push_loop_back_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:permissions:impl",
|
||||
"includeRegex" : [
|
||||
"troubleshoot_notifications_test_check_permission_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:pushproviders:firebase",
|
||||
"includeRegex" : [
|
||||
"troubleshoot_notifications_test_firebase_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:pushproviders:unifiedpush",
|
||||
"includeRegex" : [
|
||||
"troubleshoot_notifications_test_unified_push_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:login:impl",
|
||||
"includeRegex" : [
|
||||
"screen_onboarding_.*",
|
||||
"screen_login_.*",
|
||||
"screen_server_confirmation_.*",
|
||||
"screen_change_server_.*",
|
||||
"screen_change_account_provider_.*",
|
||||
"screen_create_account_.*",
|
||||
"screen_account_provider_.*",
|
||||
"screen_waitlist_.*",
|
||||
"screen_qr_code_login_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:leaveroom:api",
|
||||
"includeRegex" : [
|
||||
"leave_room_alert_.*",
|
||||
"leave_conversation_alert_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:home:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.home\\..*",
|
||||
"screen_roomlist_.*",
|
||||
"screen\\.roomlist\\..*",
|
||||
"session_verification_banner_.*",
|
||||
"confirm_recovery_key_banner_.*",
|
||||
"banner\\.set_up_recovery\\..*",
|
||||
"banner\\.battery_optimization\\..*",
|
||||
"banner\\.new_sound\\..*",
|
||||
"full_screen_intent_banner_.*",
|
||||
"screen_migration_.*",
|
||||
"screen_invites_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:roomdetails:impl",
|
||||
"includeRegex" : [
|
||||
"screen_room_details_.*",
|
||||
"screen\\.room_details\\..*",
|
||||
"screen_room_member_list_.*",
|
||||
"screen\\.room_member_list\\..*",
|
||||
"screen_room_notification_settings_.*",
|
||||
"screen_notification_settings_edit_failed_updating_default_mode",
|
||||
"screen_polls_history_title",
|
||||
"screen_notification_settings_mentions_only_disclaimer",
|
||||
"screen_room_change_.*",
|
||||
"screen_room_roles_.*",
|
||||
"screen\\.edit_room_address\\..*",
|
||||
"screen\\.security_and_privacy\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:space:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.leave_space\\..*",
|
||||
"screen\\.space_settings\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:userprofile:shared",
|
||||
"includeRegex" : [
|
||||
"screen_start_chat_error_starting_chat",
|
||||
"screen_dm_details_.*",
|
||||
"screen_room_member_details_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:invitepeople:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.invite_users\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:messages:impl",
|
||||
"includeRegex" : [
|
||||
"emoji_picker_category_.*",
|
||||
"screen_report_content_.*",
|
||||
"screen_room_attachment.*",
|
||||
"screen_room_encrypted.*",
|
||||
"screen_room_invite.*",
|
||||
"screen\\.room\\.mention.*",
|
||||
"screen_room_message.*",
|
||||
"screen_room_retry.*",
|
||||
"screen_room_timeline.*",
|
||||
"screen\\.room_timeline.*",
|
||||
"screen_room_typing.*",
|
||||
"screen\\.media_upload.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:analytics:impl",
|
||||
"includeRegex" : [
|
||||
"screen_analytics_prompt.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:analytics:api",
|
||||
"includeRegex" : [
|
||||
"screen_analytics_settings_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:ftue:impl",
|
||||
"includeRegex" : [
|
||||
"screen_welcome_.*",
|
||||
"screen_notification_optin_.*",
|
||||
"screen_identity_.*",
|
||||
"screen_session_verification_enter_recovery_key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:poll:impl",
|
||||
"includeRegex" : [
|
||||
"screen_create_poll_.*",
|
||||
"screen_edit_poll_.*",
|
||||
"screen_polls_history_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:poll:api",
|
||||
"includeRegex" : [
|
||||
"a11y\\.polls\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:securebackup:impl",
|
||||
"includeRegex" : [
|
||||
"screen_chat_backup_.*",
|
||||
"screen_key_backup_disable_.*",
|
||||
"screen_recovery_key_.*",
|
||||
"screen_create_new_recovery_key_.*",
|
||||
"screen_encryption_reset.*",
|
||||
"screen_reset_encryption.*",
|
||||
"screen\\.reset_encryption.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:preferences:impl",
|
||||
"includeRegex" : [
|
||||
"screen_advanced_settings_.*",
|
||||
"screen\\.advanced_settings\\..*",
|
||||
"screen_edit_profile_.*",
|
||||
"screen_notification_settings_.*",
|
||||
"screen_blocked_users_.*",
|
||||
"full_screen_intent_banner_.*",
|
||||
"troubleshoot_notifications_entry_point_.*",
|
||||
"screen\\.labs\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:troubleshoot:impl",
|
||||
"includeRegex" : [
|
||||
"troubleshoot_notifications_screen_.*",
|
||||
"screen\\.push_history\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:matrixui",
|
||||
"includeRegex" : [
|
||||
"screen_invites_invited_you",
|
||||
"screen\\.bottom_sheet\\.create_dm\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:call:impl",
|
||||
"includeRegex" : [
|
||||
"call_.*",
|
||||
"screen_incoming_call.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:lockscreen:impl",
|
||||
"includeRegex" : [
|
||||
"screen_app_lock_.*",
|
||||
"screen_signout_in_progress_dialog_content"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:roomdirectory:impl",
|
||||
"includeRegex" : [
|
||||
"screen_room_directory_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:joinroom:impl",
|
||||
"includeRegex" : [
|
||||
"screen_join_room_.*",
|
||||
"screen\\.join_room\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:knockrequests:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.knock_requests_list\\..*",
|
||||
"screen\\.room\\.single_knock_request.*",
|
||||
"screen\\.room\\.multiple_knock_requests.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:reportroom:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.report_room\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:roommembermoderation:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.bottom_sheet\\.manage_room_member\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:rolesandpermissions:impl",
|
||||
"includeRegex" : [
|
||||
"screen_room_change_.*",
|
||||
"screen_room_roles_.*",
|
||||
"screen\\.room_roles_and_permissions\\..*",
|
||||
"screen_room_member_list.*",
|
||||
"screen\\.room_member_list\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:securityandprivacy:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.edit_room_address\\..*",
|
||||
"screen\\.security_and_privacy\\..*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
set -e
|
||||
|
||||
if [[ $1 == "--all" ]]; then
|
||||
echo "Note: I will update all the files."
|
||||
allFiles=1
|
||||
else
|
||||
echo "Note: I will update only the English files."
|
||||
allFiles=0
|
||||
fi
|
||||
|
||||
echo "Generating the configuration file for localazy..."
|
||||
python3 ./tools/localazy/generateLocalazyConfig.py $allFiles
|
||||
|
||||
echo "Deleting all existing localazy.xml files..."
|
||||
find . -name 'localazy.xml' -delete
|
||||
|
||||
if [[ $allFiles == 1 ]]; then
|
||||
echo "Deleting all existing translations.xml files..."
|
||||
find . -name 'translations.xml' -delete
|
||||
fi
|
||||
|
||||
echo "Importing the strings..."
|
||||
localazy download --config ./tools/localazy/localazy.json
|
||||
|
||||
echo "Removing the generated config"
|
||||
rm ./tools/localazy/localazy.json
|
||||
|
||||
echo "Formatting the resources files..."
|
||||
find . -name 'localazy.xml' -exec ./tools/localazy/formatXmlResourcesFile.py {} \;
|
||||
if [[ $allFiles == 1 ]]; then
|
||||
find . -name 'translations.xml' -exec ./tools/localazy/formatXmlResourcesFile.py {} \;
|
||||
fi
|
||||
|
||||
echo "Checking forbidden terms..."
|
||||
find . -name 'localazy.xml' -exec ./tools/localazy/checkForbiddenTerms.py {} \;
|
||||
if [[ $allFiles == 1 ]]; then
|
||||
find . -name 'translations.xml' -exec ./tools/localazy/checkForbiddenTerms.py {} \;
|
||||
fi
|
||||
|
||||
echo "Success!"
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import re
|
||||
import sys
|
||||
from xml.dom import minidom
|
||||
|
||||
file = sys.argv[1]
|
||||
|
||||
content = minidom.parse(file)
|
||||
|
||||
# sort content by value of tag name
|
||||
newContent = minidom.Document()
|
||||
resources = newContent.createElement('resources')
|
||||
resources.setAttribute('xmlns:xliff', 'urn:oasis:names:tc:xliff:document:1.2')
|
||||
newContent.appendChild(resources)
|
||||
|
||||
resource = dict()
|
||||
|
||||
### Strings
|
||||
for elem in content.getElementsByTagName('string'):
|
||||
name = elem.attributes['name'].value
|
||||
# Continue if value is empty
|
||||
child = elem.firstChild
|
||||
if child is None:
|
||||
# Print an error to stderr
|
||||
print('Warning: Empty content for string: ' + name + " in file " + file, file=sys.stderr)
|
||||
continue
|
||||
value = child.nodeValue
|
||||
# Continue if string is empty
|
||||
if value == '""':
|
||||
# Print an error to stderr
|
||||
print('Warning: Empty string value for string: ' + name + " in file " + file, file=sys.stderr)
|
||||
continue
|
||||
resource[name] = elem.cloneNode(True)
|
||||
|
||||
### Plurals
|
||||
for elem in content.getElementsByTagName('plurals'):
|
||||
plural = newContent.createElement('plurals')
|
||||
name = elem.attributes['name'].value
|
||||
plural.setAttribute('name', name)
|
||||
for it in elem.childNodes:
|
||||
if it.nodeType != it.ELEMENT_NODE:
|
||||
continue
|
||||
# Continue if value is empty
|
||||
child = it.firstChild
|
||||
if child is None:
|
||||
# Print an error to stderr
|
||||
print('Warning: Empty content for plurals: ' + name + " in file " + file, file=sys.stderr)
|
||||
continue
|
||||
value = child.nodeValue
|
||||
# Continue if string is empty
|
||||
if value == '""':
|
||||
# Print an error to stderr
|
||||
print('Warning: Empty item value for plurals: ' + name + " in file " + file, file=sys.stderr)
|
||||
continue
|
||||
plural.appendChild(it.cloneNode(True))
|
||||
if plural.hasChildNodes():
|
||||
resource[name] = plural
|
||||
|
||||
for key in sorted(resource.keys()):
|
||||
resources.appendChild(resource[key])
|
||||
|
||||
result = newContent.toprettyxml(indent=" ") \
|
||||
.replace('<?xml version="1.0" ?>', '<?xml version="1.0" encoding="utf-8"?>') \
|
||||
.replace('"', '"') \
|
||||
.replace('...', '…')
|
||||
|
||||
## Replace space by unbreakable space before punctuation
|
||||
result = re.sub(r" ([\?\!\:…])", r" \1", result)
|
||||
|
||||
# Special treatment for French wording
|
||||
if 'values-fr' in file:
|
||||
## Replace ' with ’
|
||||
result = re.sub(r"([cdjlmnsu])\\\'", r"\1’", result, flags=re.IGNORECASE)
|
||||
|
||||
with open(file, "w") as text_file:
|
||||
text_file.write(result)
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Read the config.json file
|
||||
with open('./tools/localazy/config.json', 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
allFiles = sys.argv[1] == "1"
|
||||
|
||||
# Convert a module name to a path
|
||||
# Ex: ":features:verifysession:impl" => "features/verifysession/impl"
|
||||
def convertModuleToPath(name):
|
||||
return name[1:].replace(":", "/")
|
||||
|
||||
# Regex that will be excluded from the Android project, you may add items here if necessary.
|
||||
regexToAlwaysExclude = [
|
||||
"Notification",
|
||||
".*_ios"
|
||||
]
|
||||
|
||||
baseAction = {
|
||||
"type": "android",
|
||||
# Replacement done in all string values
|
||||
"replacements": {
|
||||
"...": "…"
|
||||
},
|
||||
"params": {
|
||||
"force_underscore": "yes"
|
||||
}
|
||||
}
|
||||
|
||||
# Store all regex specific to module, to exclude the corresponding key from the common string module
|
||||
allRegexToExcludeFromMainModule = []
|
||||
# All actions that will be serialized in the localazy config
|
||||
allActions = []
|
||||
|
||||
# Iterating on the config
|
||||
for entry in config["modules"]:
|
||||
# Create action for the default language
|
||||
excludeRegex = regexToAlwaysExclude
|
||||
if "excludeRegex" in entry:
|
||||
excludeRegex += entry["excludeRegex"]
|
||||
action = baseAction | {
|
||||
"output": convertModuleToPath(entry["name"]) + "/src/main/res/values/localazy.xml",
|
||||
"includeKeys": list(map(lambda i: "REGEX:" + i, entry["includeRegex"])),
|
||||
"excludeKeys": list(map(lambda i: "REGEX:" + i, excludeRegex)),
|
||||
"conditions": [
|
||||
"equals: ${langAndroidResNoScript}, en | equals: ${file}, content.json"
|
||||
]
|
||||
}
|
||||
# print(action)
|
||||
allActions.append(action)
|
||||
# Create action for the translations
|
||||
if allFiles:
|
||||
actionTranslation = baseAction | {
|
||||
"output": convertModuleToPath(entry["name"]) + "/src/main/res/values-${langAndroidResNoScript}/translations.xml",
|
||||
"includeKeys": list(map(lambda i: "REGEX:" + i, entry["includeRegex"])),
|
||||
"excludeKeys": list(map(lambda i: "REGEX:" + i, excludeRegex)),
|
||||
"conditions": [
|
||||
"!equals: ${langAndroidResNoScript}, en | equals: ${file}, content.json"
|
||||
],
|
||||
"langAliases": {
|
||||
"id": "in"
|
||||
}
|
||||
}
|
||||
allActions.append(actionTranslation)
|
||||
allRegexToExcludeFromMainModule.extend(entry["includeRegex"])
|
||||
|
||||
# Append configuration for the main string module: default language
|
||||
mainAction = baseAction | {
|
||||
"output": "libraries/ui-strings/src/main/res/values/localazy.xml",
|
||||
"excludeKeys": list(map(lambda i: "REGEX:" + i, allRegexToExcludeFromMainModule + regexToAlwaysExclude)),
|
||||
"conditions": [
|
||||
"equals: ${langAndroidResNoScript}, en | equals: ${file}, content.json"
|
||||
]
|
||||
}
|
||||
# print(mainAction)
|
||||
allActions.append(mainAction)
|
||||
|
||||
if allFiles:
|
||||
# Append configuration for the main string module: translations
|
||||
mainActionTranslation = baseAction | {
|
||||
"output": "libraries/ui-strings/src/main/res/values-${langAndroidResNoScript}/translations.xml",
|
||||
"excludeKeys": list(map(lambda i: "REGEX:" + i, allRegexToExcludeFromMainModule + regexToAlwaysExclude)),
|
||||
"conditions": [
|
||||
"!equals: ${langAndroidResNoScript}, en | equals: ${file}, content.json"
|
||||
],
|
||||
"langAliases": {
|
||||
"id": "in"
|
||||
}
|
||||
}
|
||||
allActions.append(mainActionTranslation)
|
||||
|
||||
# Generate the configuration for localazy
|
||||
result = {
|
||||
"readKey": "a7876306080832595063-aa37154bb3772f6146890fca868d155b2228b492c56c91f67abdcdfb74d6142d",
|
||||
"conversion": {
|
||||
"actions": allActions
|
||||
}
|
||||
}
|
||||
|
||||
# Json serialization
|
||||
with open('./tools/localazy/localazy.json', 'w') as json_file:
|
||||
json.dump(result, json_file, indent=4, sort_keys=True)
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
def getLocalesFromLocalazy():
|
||||
command = subprocess.run(
|
||||
["localazy languages --read-key a7876306080832595063-aa37154bb3772f6146890fca868d155b2228b492c56c91f67abdcdfb74d6142d --csv"],
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
data = command.stdout
|
||||
result = []
|
||||
for line in data.split("\n"):
|
||||
if line:
|
||||
line = line.split(",")
|
||||
if (line[6] == "true"):
|
||||
result.append(line[0])
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def normalizeForResourceConfigurations(locale):
|
||||
match locale:
|
||||
case "id":
|
||||
return "in"
|
||||
case "zh_TW#Hant":
|
||||
return "zh-rTW"
|
||||
case "pt_BR":
|
||||
return "pt-rBR"
|
||||
case "zh#Hans":
|
||||
return "zh-rCN"
|
||||
case "en_US":
|
||||
return "en-rUS"
|
||||
case _:
|
||||
return locale
|
||||
|
||||
|
||||
def normalizeForLocalConfig(locale):
|
||||
match locale:
|
||||
case "id":
|
||||
return "in"
|
||||
case "zh_TW#Hant":
|
||||
return "zh-TW"
|
||||
case "zh#Hans":
|
||||
return "zh-CN"
|
||||
case _:
|
||||
return locale
|
||||
|
||||
|
||||
def generateLocaleFile(locales, file):
|
||||
with open("plugins/src/main/kotlin/extension/locales.kt", "w") as f:
|
||||
f.write("// File generated by " + file + ", do not edit\n\n")
|
||||
f.write("package extension\n\n")
|
||||
f.write("val locales = setOf(\n")
|
||||
for locale in locales:
|
||||
f.write(" \"" + normalizeForResourceConfigurations(locale) + "\",\n")
|
||||
f.write(")\n")
|
||||
|
||||
|
||||
def generateLocalesConfigFile(locales, file):
|
||||
with open("app/src/main/res/xml/locales_config.xml", "w") as f:
|
||||
f.write("<!-- File generated by " + file + ", do not edit -->\n")
|
||||
f.write('<locale-config xmlns:android="http://schemas.android.com/apk/res/android">\n')
|
||||
for locale in locales:
|
||||
f.write(" <locale android:name=\"" + normalizeForLocalConfig(locale) + "\"/>\n")
|
||||
f.write("</locale-config>\n")
|
||||
|
||||
|
||||
def main():
|
||||
file = os.path.basename(__file__)
|
||||
locales = getLocalesFromLocalazy()
|
||||
generateLocaleFile(locales, file)
|
||||
generateLocalesConfigFile(locales, file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user