diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2021-07-07 09:55:27 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2021-07-07 09:55:27 +0200 |
| commit | d4c815a12e8622699ab4609ec7aafcaf3480b8e8 (patch) | |
| tree | 35144492c3a3dfc5442fb87277b78fcb1e4f4d76 | |
| parent | 86bb6d0efb5d93cfd3054b1dc7d5cddcd1575d04 (diff) | |
| parent | 4b570d1b1928bdd7505d4696793444a588fb5b28 (diff) | |
| download | mullvadvpn-d4c815a12e8622699ab4609ec7aafcaf3480b8e8.tar.xz mullvadvpn-d4c815a12e8622699ab4609ec7aafcaf3480b8e8.zip | |
Merge branch 'ios-localization-automation'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 19 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppStorePaymentManager.swift | 6 | ||||
| -rw-r--r-- | ios/MullvadVPN/en.lproj/AppStoreSubscriptions.strings | 2 | ||||
| -rw-r--r-- | ios/README.md | 27 | ||||
| -rw-r--r-- | ios/pylintrc | 14 | ||||
| -rw-r--r-- | ios/requirements.txt | 42 | ||||
| -rw-r--r-- | ios/update_translations.py | 194 |
7 files changed, 303 insertions, 1 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 27fe2acc16..e69c082ba1 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ 582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */; }; 582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */; }; 582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B42295780F0055B6EF /* AccountExpiry.swift */; }; + 582CFEE726945FC30072883A /* AppStoreSubscriptions.strings in Resources */ = {isa = PBXBuildFile; fileRef = 582CFEE526945FC30072883A /* AppStoreSubscriptions.strings */; }; 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; }; 583BC70724FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.swift */; }; 583BC70824FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.swift */; }; @@ -324,6 +325,7 @@ 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationBar.swift; sourceTree = "<group>"; }; 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountCell.swift; sourceTree = "<group>"; }; 582BB1B42295780F0055B6EF /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; }; + 582CFEE626945FC30072883A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppStoreSubscriptions.strings; sourceTree = "<group>"; }; 5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = "<group>"; }; 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+DispatchQueue.swift"; sourceTree = "<group>"; }; 583DA21325FA4B5C00318683 /* LocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSource.swift; sourceTree = "<group>"; }; @@ -531,6 +533,14 @@ path = Logging; sourceTree = "<group>"; }; + 582CFEE1269448160072883A /* Localizations */ = { + isa = PBXGroup; + children = ( + 582CFEE526945FC30072883A /* AppStoreSubscriptions.strings */, + ); + name = Localizations; + sourceTree = "<group>"; + }; 586ADD4323FC13AD00CE9E87 /* GeoJSON */ = { isa = PBXGroup; children = ( @@ -935,6 +945,7 @@ 586ADD4723FC13F400CE9E87 /* countries.geo.json in Resources */, 58CE5E6B224146210008646E /* Assets.xcassets in Resources */, 5883A09E266A5AF7003EFFCB /* Localizable.strings in Resources */, + 582CFEE726945FC30072883A /* AppStoreSubscriptions.strings in Resources */, 584789B8264D4A2A000E45FB /* old_le_root_cert.cer in Resources */, 584789BE264D4A2A000E45FB /* new_le_root_cert.cer in Resources */, 58E5BC2624FEB6DB00A53A76 /* AccountViewController.xib in Resources */, @@ -1232,6 +1243,14 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 582CFEE526945FC30072883A /* AppStoreSubscriptions.strings */ = { + isa = PBXVariantGroup; + children = ( + 582CFEE626945FC30072883A /* en */, + ); + name = AppStoreSubscriptions.strings; + sourceTree = "<group>"; + }; 587B7543266922BF00DEF7E9 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( diff --git a/ios/MullvadVPN/AppStorePaymentManager.swift b/ios/MullvadVPN/AppStorePaymentManager.swift index 4fa043b9c5..407b1956f5 100644 --- a/ios/MullvadVPN/AppStorePaymentManager.swift +++ b/ios/MullvadVPN/AppStorePaymentManager.swift @@ -17,7 +17,11 @@ enum AppStoreSubscription: String { var localizedTitle: String { switch self { case .thirtyDays: - return NSLocalizedString("Add 30 days time", comment: "") + return NSLocalizedString( + "APPSTORE_SUBSCRIPTION_TITLE_ADD_30_DAYS", + tableName: "AppStoreSubscriptions", + comment: "Title for non-renewable subscription that credits 30 days to user account." + ) } } } diff --git a/ios/MullvadVPN/en.lproj/AppStoreSubscriptions.strings b/ios/MullvadVPN/en.lproj/AppStoreSubscriptions.strings new file mode 100644 index 0000000000..6624ce8273 --- /dev/null +++ b/ios/MullvadVPN/en.lproj/AppStoreSubscriptions.strings @@ -0,0 +1,2 @@ +/* Title for non-renewable subscription that credits 30 days to user account. */ +"APPSTORE_SUBSCRIPTION_TITLE_ADD_30_DAYS" = "Add 30 days time"; diff --git a/ios/README.md b/ios/README.md index f7d450a213..160245eeee 100644 --- a/ios/README.md +++ b/ios/README.md @@ -52,3 +52,30 @@ bundle exec fastlane snapshot ``` Once done all screenshots should be saved under `ios/Screenshots` folder. + +### Localizations + +#### Update localizations from source + +Run the following command in terminal: + +``` +python3 update_localizations.py +``` + +#### Locking Python dependencies + +1. Freeze dependencies: + +``` +pip3 freeze -r requirements.txt +``` + +and save the output into `requirements.txt`. + + +2. Hash them with `hashin` tool: + +``` +hashin --python 3.7 --verbose --update-all +```
\ No newline at end of file diff --git a/ios/pylintrc b/ios/pylintrc new file mode 100644 index 0000000000..35ec9d3f74 --- /dev/null +++ b/ios/pylintrc @@ -0,0 +1,14 @@ +[FORMAT] +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=2 + +[MESSAGES CONTROL] + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). diff --git a/ios/requirements.txt b/ios/requirements.txt new file mode 100644 index 0000000000..a2a01cd1d5 --- /dev/null +++ b/ios/requirements.txt @@ -0,0 +1,42 @@ +nslocalized==0.2.0 \ + --hash=sha256:46d6f776b26e9da0e0d0c759d99782884882cade59d5c935005a85cba654db9e +## The following requirements were added by pip freeze: +astroid==2.6.2 +cffi==1.14.5 \ + --hash=sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69 \ + --hash=sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06 \ + --hash=sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05 \ + --hash=sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73 \ + --hash=sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49 \ + --hash=sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4 \ + --hash=sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62 \ + --hash=sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc \ + --hash=sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1 \ + --hash=sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c +chardet==4.0.0 \ + --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \ + --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 +click==8.0.1 \ + --hash=sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a \ + --hash=sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6 +GDAL==3.3.1 \ + --hash=sha256:776ad8f499919d6824b7ba3ae8bd53b2df5d73a14dc047ebc8f33f9507c8dcc5 +hashin==0.15.0 +igenstrings==1.1.0 +isort==5.9.1 +lazy-object-proxy==1.6.0 +mccabe==0.6.1 +packaging==21.0 +pip-api==0.0.20 +pycparser==2.20 +pylint==2.9.3 +pyparsing==2.4.7 +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 +tinycss2==1.1.0 \ + --hash=sha256:0353b5234bcaee7b1ac7ca3dea7e02cd338a9f8dcbb8f2dcd32a5795ec1e5f9a \ + --hash=sha256:fbdcac3044d60eb85fdb2aa840ece43cf7dbe798e373e6ee0be545d4d134e18a +toml==0.10.2 +webencodings==0.5.1 +wrapt==1.12.1
\ No newline at end of file diff --git a/ios/update_translations.py b/ios/update_translations.py new file mode 100644 index 0000000000..51a9f10068 --- /dev/null +++ b/ios/update_translations.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 + +""" +A helper script to parse NSLocalizedString in Swift source files and extract and merge new +translations with the existing ones. +""" + +import os +from os import path +from subprocess import Popen, PIPE +import shutil +from nslocalized import StringTable + +# Current script dir +SCRIPT_DIR = path.dirname(path.realpath(__file__)) + +# Path to directory with source files (Swift) +SOURCE_PATH = path.join(SCRIPT_DIR, "MullvadVPN") + +# Path to directory with base localizations +BASE_LANGUAGE_PATH = path.join(SOURCE_PATH, "en.lproj") + +# Path to directory with the output of genstrings tool. +GENSTRINGS_OUTPUT_PATH = path.join(SCRIPT_DIR, "genstrings-out") + +# Output encoding for strings files. +# +# By default genstrings tool outputs text in utf-16, which git recognizes as binary, therefore +# rendering diff capability useless. +# +# Store localization files in utf-8 to fix this. Xcode automatically transcodes localization +# files to utf-16 during the build phase. +OUTPUT_ENCOODING = "utf_8" + + +def check_file_extension(file, expected_extension): + """ + Returns True if the file extension matches the expected one. + """ + (_basename, ext) = os.path.splitext(file) + return ext == expected_extension + + +def get_source_files(): + """ + Find all Swift source files recursively. + """ + results = [] + for root, _dirs, files in os.walk(SOURCE_PATH): + for file in files: + if check_file_extension(file, ".swift"): + results.append(path.join(root, file)) + return results + + +def get_strings_files(dir_path): + """ + Find all .strings files within the given directory. + """ + results = [] + for file in os.listdir(dir_path): + if check_file_extension(file, ".strings"): + results.append(file) + return results + + +def create_empty_output_dir(): + """ + Creates empty directory for output of genstrings tool. + """ + # Wipe out old files + delete_output_dir() + + # Re-create out directory + os.mkdir(GENSTRINGS_OUTPUT_PATH) + + +def delete_output_dir(): + """ + Delete directory used for genstrings output + """ + if path.exists(GENSTRINGS_OUTPUT_PATH): + shutil.rmtree(GENSTRINGS_OUTPUT_PATH) + + +def extract_translations(): + """ + Extract translations from sources using genstrings tool. + """ + + # Get Swift source files + source_files = get_source_files() + + # Genstrings utility comes with Xcode and used for extracting the localizable strings from source + # files and producing string tables. + args = ( + "genstrings", "-o", GENSTRINGS_OUTPUT_PATH, + *source_files + ) + (exit_code, errors) = run_program(*args) + + if exit_code == 0: + print("Genstrings finished without errors.") + else: + print("Genstrings exited with {}: {}".format(exit_code, errors.decode().strip())) + + +def merge_translations(): + """ + Merge string tables, delete stale ones and copy new ones. + """ + + # Existing string tables + existing_string_tables = get_strings_files(BASE_LANGUAGE_PATH) + + # Newly generated string tables + new_string_tables = get_strings_files(GENSTRINGS_OUTPUT_PATH) + + # String tables that will be merged + merge_string_tables = [] + + # Detect new string tables + for table_name in new_string_tables: + if table_name not in existing_string_tables: + src = path.join(GENSTRINGS_OUTPUT_PATH, table_name) + dst = path.join(BASE_LANGUAGE_PATH, table_name) + + print("Copying {} to {}".format(src, dst)) + new_table = StringTable.read(src) + new_table.write(dst, encoding=OUTPUT_ENCOODING) + + # Detect removed string tables + for table_name in existing_string_tables: + if table_name in new_string_tables: + merge_string_tables.append(table_name) + else: + filepath = path.join(BASE_LANGUAGE_PATH, table_name) + + print("Removing {}".format(filepath)) + os.unlink(filepath) + + # Merge remaining string tables + for table_name in merge_string_tables: + new_table_path = path.join(GENSTRINGS_OUTPUT_PATH, table_name) + base_table_path = path.join(BASE_LANGUAGE_PATH, table_name) + + merge_two_tables(base_table_path, new_table_path) + + +def merge_two_tables(base_table_path, new_table_path): + """ + Merge new table into base table. + """ + # Existing string table previously generated from sources + base_table = StringTable.read(base_table_path) + + # New string table generated from sources + new_table = StringTable.read(new_table_path) + + print("Merging {} into {}".format(new_table_path, base_table_path)) + + # Iterate through newly generated table and preserve existing translations. + for new_key in new_table.strings: + base_entry = base_table.lookup(new_key) + new_entry = new_table.lookup(new_key) + if base_entry is not None: + new_entry.target = base_entry.target + + print("Write {} on disk.".format(base_table_path)) + new_table.write(base_table_path, encoding=OUTPUT_ENCOODING) + + +def run_program(*args): + """ + Run program and return a tuple with returncode and errors. + """ + with Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) as subproc: + print("Run: {}".format(' '.join(args))) + + errors = subproc.communicate()[1] + return (subproc.returncode, errors) + + +def main(): + """ + Program entry + """ + create_empty_output_dir() + extract_translations() + merge_translations() + delete_output_dir() + + +main() |
