summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2021-07-07 09:55:27 +0200
committerAndrej Mihajlov <and@mullvad.net>2021-07-07 09:55:27 +0200
commitd4c815a12e8622699ab4609ec7aafcaf3480b8e8 (patch)
tree35144492c3a3dfc5442fb87277b78fcb1e4f4d76
parent86bb6d0efb5d93cfd3054b1dc7d5cddcd1575d04 (diff)
parent4b570d1b1928bdd7505d4696793444a588fb5b28 (diff)
downloadmullvadvpn-d4c815a12e8622699ab4609ec7aafcaf3480b8e8.tar.xz
mullvadvpn-d4c815a12e8622699ab4609ec7aafcaf3480b8e8.zip
Merge branch 'ios-localization-automation'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj19
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager.swift6
-rw-r--r--ios/MullvadVPN/en.lproj/AppStoreSubscriptions.strings2
-rw-r--r--ios/README.md27
-rw-r--r--ios/pylintrc14
-rw-r--r--ios/requirements.txt42
-rw-r--r--ios/update_translations.py194
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()