dotstrings 2.0.0__tar.gz → 3.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dotstrings-2.0.0 → dotstrings-3.1.0}/PKG-INFO +7 -5
- {dotstrings-2.0.0 → dotstrings-3.1.0}/dotstrings/__init__.py +6 -6
- {dotstrings-2.0.0 → dotstrings-3.1.0}/dotstrings/dot_strings_entry.py +2 -4
- {dotstrings-2.0.0 → dotstrings-3.1.0}/dotstrings/dot_stringsdict_entry.py +15 -13
- dotstrings-3.1.0/dotstrings/exceptions.py +5 -0
- {dotstrings-2.0.0 → dotstrings-3.1.0}/dotstrings/genstrings.py +7 -6
- {dotstrings-2.0.0 → dotstrings-3.1.0}/dotstrings/localized_bundle.py +15 -14
- {dotstrings-2.0.0 → dotstrings-3.1.0}/dotstrings/localized_string.py +22 -18
- {dotstrings-2.0.0 → dotstrings-3.1.0}/dotstrings/parser.py +33 -17
- {dotstrings-2.0.0 → dotstrings-3.1.0}/pyproject.toml +8 -8
- {dotstrings-2.0.0 → dotstrings-3.1.0}/LICENSE +0 -0
- {dotstrings-2.0.0 → dotstrings-3.1.0}/README.md +0 -0
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dotstrings
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: Tools for dealing with the .strings files for iOS and macOS
|
|
5
5
|
Home-page: https://github.com/Microsoft/dotstrings
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: localization,iOS,macOS,strings
|
|
8
8
|
Author: Dale Myers
|
|
9
|
-
Author-email:
|
|
10
|
-
Requires-Python: >=3.
|
|
9
|
+
Author-email: dalemyers@microsoft.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Environment :: Console
|
|
13
13
|
Classifier: Environment :: MacOS X
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
23
|
Classifier: Topic :: Software Development
|
|
22
24
|
Classifier: Topic :: Utilities
|
|
23
25
|
Project-URL: Repository, https://github.com/Microsoft/dotstrings
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""Utilities for dealing with .strings files"""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from typing import Dict, List, Optional, Set
|
|
5
4
|
|
|
6
5
|
from dotstrings.parser import load, loads, load_dict, loads_dict
|
|
7
6
|
from dotstrings.dot_strings_entry import DotStringsEntry
|
|
8
7
|
from dotstrings.dot_stringsdict_entry import DotStringsDictEntry, Variable
|
|
8
|
+
from dotstrings.exceptions import DotStringsException
|
|
9
9
|
from dotstrings.localized_bundle import LocalizedBundle
|
|
10
10
|
from dotstrings.localized_string import LocalizedString
|
|
11
11
|
|
|
@@ -50,7 +50,7 @@ def stringsdict_file_path(stringsdict_folder: str, language: str, table_name: st
|
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def languages_in_folder(strings_folder: str) ->
|
|
53
|
+
def languages_in_folder(strings_folder: str) -> set[str]:
|
|
54
54
|
"""Find all the languages in a folder
|
|
55
55
|
|
|
56
56
|
This looks for *.lproj folders.
|
|
@@ -65,7 +65,7 @@ def languages_in_folder(strings_folder: str) -> Set[str]:
|
|
|
65
65
|
return {language.replace(".lproj", "") for language in languages}
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
def load_table(strings_folder: str, language: str, table_name: str) ->
|
|
68
|
+
def load_table(strings_folder: str, language: str, table_name: str) -> list[LocalizedString]:
|
|
69
69
|
"""Load the specified .strings table
|
|
70
70
|
|
|
71
71
|
:param strings_folder: The location of the strings folder (which contains
|
|
@@ -83,7 +83,7 @@ def load_table(strings_folder: str, language: str, table_name: str) -> List[Loca
|
|
|
83
83
|
)
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def load_language_tables(strings_folder: str, language: str) ->
|
|
86
|
+
def load_language_tables(strings_folder: str, language: str) -> dict[str, list[LocalizedString]]:
|
|
87
87
|
"""Load the .strings tables for a given language
|
|
88
88
|
|
|
89
89
|
:param strings_folder: The location of the strings folder (which contains
|
|
@@ -129,7 +129,7 @@ def load_all_strings(strings_folder: str) -> LocalizedBundle:
|
|
|
129
129
|
|
|
130
130
|
def normalize(
|
|
131
131
|
strings_path: str,
|
|
132
|
-
output_path:
|
|
132
|
+
output_path: str | None = None,
|
|
133
133
|
remove_duplicates: bool = True,
|
|
134
134
|
sort_comments: bool = True,
|
|
135
135
|
) -> None:
|
|
@@ -165,7 +165,7 @@ def normalize(
|
|
|
165
165
|
# If we have duplicate keys but the values don't match, that's an
|
|
166
166
|
# exception, whether or not we are removing duplicates
|
|
167
167
|
if deduped_entries[-1].value != entry.value or not remove_duplicates:
|
|
168
|
-
raise
|
|
168
|
+
raise DotStringsException(f"Found duplicate strings with key: {entry.key}")
|
|
169
169
|
|
|
170
170
|
deduped_entries[-1].comments.extend(entry.comments)
|
|
171
171
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Base types for the dotstrings library."""
|
|
2
2
|
|
|
3
|
-
from typing import List
|
|
4
|
-
|
|
5
3
|
|
|
6
4
|
class DotStringsEntry:
|
|
7
5
|
"""Represents a .strings entry.
|
|
@@ -13,9 +11,9 @@ class DotStringsEntry:
|
|
|
13
11
|
|
|
14
12
|
key: str
|
|
15
13
|
value: str
|
|
16
|
-
comments:
|
|
14
|
+
comments: list[str]
|
|
17
15
|
|
|
18
|
-
def __init__(self, key: str, value: str, comments:
|
|
16
|
+
def __init__(self, key: str, value: str, comments: list[str]) -> None:
|
|
19
17
|
self.key = key
|
|
20
18
|
self.value = value
|
|
21
19
|
self.comments = comments
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Base types for the dotstrings library."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from dotstrings.exceptions import DotStringsException
|
|
4
4
|
|
|
5
5
|
VARIABLE_VALUE_TYPE_KEY = "NSStringFormatValueTypeKey"
|
|
6
6
|
VARIABLE_VALUE_SPEC_KEY = "NSStringFormatSpecTypeKey"
|
|
@@ -21,13 +21,13 @@ class Variable:
|
|
|
21
21
|
:param other_value: Value for other
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
value_type:
|
|
25
|
-
zero_value:
|
|
26
|
-
one_value:
|
|
27
|
-
two_value:
|
|
28
|
-
few_value:
|
|
29
|
-
many_value:
|
|
30
|
-
other_value:
|
|
24
|
+
value_type: str | None
|
|
25
|
+
zero_value: str | None
|
|
26
|
+
one_value: str | None
|
|
27
|
+
two_value: str | None
|
|
28
|
+
few_value: str | None
|
|
29
|
+
many_value: str | None
|
|
30
|
+
other_value: str | None
|
|
31
31
|
|
|
32
32
|
def __init__(self) -> None:
|
|
33
33
|
self.value_type = None
|
|
@@ -50,14 +50,16 @@ class Variable:
|
|
|
50
50
|
|
|
51
51
|
variable = Variable()
|
|
52
52
|
if VARIABLE_VALUE_SPEC_KEY not in contents:
|
|
53
|
-
raise
|
|
53
|
+
raise DotStringsException("NSStringFormatSpecTypeKey missing in entry")
|
|
54
54
|
|
|
55
55
|
if contents[VARIABLE_VALUE_SPEC_KEY] != VARIABLE_VALUE_SPEC_PLURAL:
|
|
56
|
-
raise
|
|
56
|
+
raise DotStringsException(
|
|
57
|
+
"Value of NSStringFormatSpecTypeKey is not NSStringPluralRuleType"
|
|
58
|
+
)
|
|
57
59
|
|
|
58
60
|
# When initializing from a dict (parsing), be sure NSStringFormatValueTypeKey exists
|
|
59
61
|
if VARIABLE_VALUE_TYPE_KEY not in contents:
|
|
60
|
-
raise
|
|
62
|
+
raise DotStringsException("NSStringFormatValueTypeKey missing in entry")
|
|
61
63
|
|
|
62
64
|
variable.value_type = contents[VARIABLE_VALUE_TYPE_KEY]
|
|
63
65
|
|
|
@@ -116,7 +118,7 @@ class DotStringsDictEntry:
|
|
|
116
118
|
|
|
117
119
|
:returns: The dict representation of this entry
|
|
118
120
|
"""
|
|
119
|
-
result = {}
|
|
121
|
+
result: dict[str, str | dict[str, str]] = {}
|
|
120
122
|
result[FORMAT_KEY] = self.value
|
|
121
123
|
for variable_name, variable in self.variables.items():
|
|
122
124
|
variable_dict = {}
|
|
@@ -165,7 +167,7 @@ class DotStringsDictEntry:
|
|
|
165
167
|
:returns: The parsed stringsdict entry
|
|
166
168
|
"""
|
|
167
169
|
if FORMAT_KEY not in contents:
|
|
168
|
-
raise
|
|
170
|
+
raise DotStringsException("NSStringLocalizedFormatKey missing in entry")
|
|
169
171
|
|
|
170
172
|
entry_format = contents[FORMAT_KEY]
|
|
171
173
|
|
|
@@ -4,7 +4,8 @@ import os
|
|
|
4
4
|
import shutil
|
|
5
5
|
import subprocess
|
|
6
6
|
import tempfile
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
from dotstrings.exceptions import DotStringsException
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def _convert_to_utf8(file_path: str) -> None:
|
|
@@ -22,13 +23,13 @@ def _convert_to_utf8(file_path: str) -> None:
|
|
|
22
23
|
iconv_command = f'iconv -f UTF-16 -t UTF-8 "{file_path}" > "{temp_file_path}"'
|
|
23
24
|
|
|
24
25
|
if subprocess.run(iconv_command, shell=True, check=False).returncode != 0:
|
|
25
|
-
raise
|
|
26
|
+
raise DotStringsException("Unable to convert from UTF-16 to UTF-8!")
|
|
26
27
|
|
|
27
28
|
shutil.move(temp_file_path, file_path)
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def generate_strings(
|
|
31
|
-
*, output_directory: str, file_paths:
|
|
32
|
+
*, output_directory: str, file_paths: list[str], clear_existing: bool = True
|
|
32
33
|
) -> None:
|
|
33
34
|
"""Run the genstrings command over the files passed in.
|
|
34
35
|
|
|
@@ -53,7 +54,7 @@ def generate_strings(
|
|
|
53
54
|
|
|
54
55
|
:param str output_directory: The location to place the output files (this
|
|
55
56
|
folder will contain an en.lproj folder after)
|
|
56
|
-
:param
|
|
57
|
+
:param list[str] file_paths: The paths to the files that should be scanned
|
|
57
58
|
:param bool clear_existing: Set to True when the existing files in the
|
|
58
59
|
output directory should be wiped before
|
|
59
60
|
generating the new strings. Defaults to True.
|
|
@@ -110,9 +111,9 @@ def generate_strings(
|
|
|
110
111
|
output = output.strip()
|
|
111
112
|
|
|
112
113
|
if len(output) > 0:
|
|
113
|
-
raise
|
|
114
|
+
raise DotStringsException(f"Encountered an error generating strings: {output}")
|
|
114
115
|
except subprocess.CalledProcessError as ex:
|
|
115
|
-
raise
|
|
116
|
+
raise DotStringsException(f"Unable generate .strings files! {ex}") from ex
|
|
116
117
|
|
|
117
118
|
# Convert all .strings files to UTF-8
|
|
118
119
|
for file_name in os.listdir(english_strings_directory):
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Representation of a localized bundle."""
|
|
2
2
|
|
|
3
|
-
from typing import cast
|
|
3
|
+
from typing import cast
|
|
4
4
|
|
|
5
|
+
from dotstrings.exceptions import DotStringsException
|
|
5
6
|
from dotstrings.localized_string import LocalizedString
|
|
6
7
|
|
|
7
8
|
|
|
@@ -11,17 +12,17 @@ class LocalizedBundle:
|
|
|
11
12
|
:param raw_entries: The raw dictionary entries that were parsed from disk
|
|
12
13
|
"""
|
|
13
14
|
|
|
14
|
-
def __init__(self, raw_entries:
|
|
15
|
+
def __init__(self, raw_entries: dict[str, dict[str, list[LocalizedString]]]) -> None:
|
|
15
16
|
self.raw_entries = raw_entries
|
|
16
17
|
|
|
17
|
-
def languages(self) ->
|
|
18
|
+
def languages(self) -> list[str]:
|
|
18
19
|
"""Return the languages supported in the bundle
|
|
19
20
|
|
|
20
21
|
:returns: A list of language codes
|
|
21
22
|
"""
|
|
22
23
|
return list(self.raw_entries.keys())
|
|
23
24
|
|
|
24
|
-
def table_names(self, validate_identical: bool = False) ->
|
|
25
|
+
def table_names(self, validate_identical: bool = False) -> list[str]:
|
|
25
26
|
"""Return the tables in the bundle.
|
|
26
27
|
|
|
27
28
|
:param validate_identical: Set to True to confirm all languages have the
|
|
@@ -34,7 +35,7 @@ class LocalizedBundle:
|
|
|
34
35
|
|
|
35
36
|
# Build up a map of languages to table names
|
|
36
37
|
|
|
37
|
-
found_tables:
|
|
38
|
+
found_tables: dict[str, set[str]] = {}
|
|
38
39
|
for language, table_map in self.raw_entries.items():
|
|
39
40
|
# table_map is a dictionary of names to lists of strings
|
|
40
41
|
found_tables[language] = set(table_map.keys())
|
|
@@ -50,13 +51,13 @@ class LocalizedBundle:
|
|
|
50
51
|
missing_tables = base_language_tables - table_names
|
|
51
52
|
|
|
52
53
|
if len(extra_tables) > 0:
|
|
53
|
-
raise
|
|
54
|
+
raise DotStringsException(
|
|
54
55
|
f"The following table names were in {language}"
|
|
55
56
|
+ f" but not in {base_language}: {extra_tables}"
|
|
56
57
|
)
|
|
57
58
|
|
|
58
59
|
if len(missing_tables) > 0:
|
|
59
|
-
raise
|
|
60
|
+
raise DotStringsException(
|
|
60
61
|
f"The following table names were in {base_language}"
|
|
61
62
|
+ f" but not in {language}: {missing_tables}"
|
|
62
63
|
)
|
|
@@ -71,7 +72,7 @@ class LocalizedBundle:
|
|
|
71
72
|
|
|
72
73
|
return list(all_table_names)
|
|
73
74
|
|
|
74
|
-
def tables_for_language(self, language: str) ->
|
|
75
|
+
def tables_for_language(self, language: str) -> dict[str, list[LocalizedString]]:
|
|
75
76
|
"""Return the tables for a language.
|
|
76
77
|
|
|
77
78
|
:param language: The language to get the tables for
|
|
@@ -82,13 +83,13 @@ class LocalizedBundle:
|
|
|
82
83
|
result = self.raw_entries.get(language, sentinel)
|
|
83
84
|
|
|
84
85
|
if result is sentinel:
|
|
85
|
-
raise
|
|
86
|
+
raise DotStringsException(f"There were no entries for language: {language}")
|
|
86
87
|
|
|
87
|
-
return cast(
|
|
88
|
+
return cast(dict[str, list[LocalizedString]], result)
|
|
88
89
|
|
|
89
90
|
def table_for_languages(
|
|
90
91
|
self, table: str, *, allow_missing: bool = False
|
|
91
|
-
) ->
|
|
92
|
+
) -> dict[str, list[LocalizedString]]:
|
|
92
93
|
"""Return a dictionary of languages to strings for a given table.
|
|
93
94
|
|
|
94
95
|
:param table: The table to load the data for
|
|
@@ -106,15 +107,15 @@ class LocalizedBundle:
|
|
|
106
107
|
continue
|
|
107
108
|
|
|
108
109
|
if table_data is sentinel and not allow_missing:
|
|
109
|
-
raise
|
|
110
|
+
raise DotStringsException(f"Could not find table {table} for language {language}")
|
|
110
111
|
|
|
111
|
-
results[language] = cast(
|
|
112
|
+
results[language] = cast(list[LocalizedString], table_data)
|
|
112
113
|
|
|
113
114
|
return results
|
|
114
115
|
|
|
115
116
|
def tables(
|
|
116
117
|
self, *, validate_missing: bool = True
|
|
117
|
-
) ->
|
|
118
|
+
) -> dict[str, dict[str, list[LocalizedString]]]:
|
|
118
119
|
"""Return the entries in the bundle, first keyed by table, then by language.
|
|
119
120
|
|
|
120
121
|
:param bool validate_missing: Set to False to disable the check that a table exists for every language
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import re
|
|
5
|
-
from typing import ClassVar,
|
|
5
|
+
from typing import ClassVar, Pattern
|
|
6
6
|
|
|
7
7
|
from dotstrings.dot_strings_entry import DotStringsEntry
|
|
8
|
+
from dotstrings.exceptions import DotStringsException
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class LocalizedString:
|
|
@@ -20,41 +21,42 @@ class LocalizedString:
|
|
|
20
21
|
you may wish to automatically generate keys to avoid having to manually
|
|
21
22
|
deal with deduplication.
|
|
22
23
|
|
|
23
|
-
:param
|
|
24
|
+
:param str | None key: The key for the string. If this is None, they key
|
|
24
25
|
will be automatically derived from the value and
|
|
25
26
|
the key extension.
|
|
26
27
|
:param str value: The value of the string
|
|
27
28
|
:param str language: The language code of the string
|
|
28
29
|
:param str table: The string table to use
|
|
29
|
-
:param
|
|
30
|
-
:param
|
|
30
|
+
:param str | None comment: The comment for the string
|
|
31
|
+
:param str | None key_extension: The key extension to differentiate
|
|
31
32
|
between identical strings with different
|
|
32
33
|
meanings
|
|
33
34
|
:param str bundle: The bundle the string can be found in
|
|
34
35
|
"""
|
|
35
36
|
|
|
36
|
-
_TOKEN_REGEX: ClassVar[
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
_TOKEN_REGEX: ClassVar[str] = (
|
|
38
|
+
r"(%(?:[0-9]+\$)?[0-9]*\.?[0-9]*[a-zA-Z]{0,2}[dDuUxXoOfFeEgGcCsSaAp@])"
|
|
39
|
+
)
|
|
39
40
|
_TOKEN_PATTERN: ClassVar[Pattern] = re.compile(_TOKEN_REGEX, flags=re.DOTALL)
|
|
40
41
|
|
|
41
42
|
key: str
|
|
42
43
|
value: str
|
|
43
44
|
language: str
|
|
44
45
|
table: str
|
|
45
|
-
comment:
|
|
46
|
-
key_extension:
|
|
46
|
+
comment: str | None
|
|
47
|
+
key_extension: str | None
|
|
47
48
|
bundle: str
|
|
48
49
|
|
|
50
|
+
# pylint: disable=too-many-arguments
|
|
49
51
|
def __init__(
|
|
50
52
|
self,
|
|
51
53
|
*,
|
|
52
|
-
key:
|
|
54
|
+
key: str | None,
|
|
53
55
|
value: str,
|
|
54
56
|
language: str,
|
|
55
57
|
table: str,
|
|
56
|
-
comment:
|
|
57
|
-
key_extension:
|
|
58
|
+
comment: str | None = None,
|
|
59
|
+
key_extension: str | None = None,
|
|
58
60
|
bundle: str = "",
|
|
59
61
|
) -> None:
|
|
60
62
|
self.value = value
|
|
@@ -69,8 +71,10 @@ class LocalizedString:
|
|
|
69
71
|
else:
|
|
70
72
|
self.key = LocalizedString._calculate_key(value=value, key_extension=key_extension)
|
|
71
73
|
|
|
74
|
+
# pylint: enable=too-many-arguments
|
|
75
|
+
|
|
72
76
|
@staticmethod
|
|
73
|
-
def _calculate_key(*, value: str, key_extension:
|
|
77
|
+
def _calculate_key(*, value: str, key_extension: str | None) -> str:
|
|
74
78
|
"""Calculate the unique key to use for this string.
|
|
75
79
|
|
|
76
80
|
:param value: The value of the localized string
|
|
@@ -93,7 +97,7 @@ class LocalizedString:
|
|
|
93
97
|
|
|
94
98
|
return key
|
|
95
99
|
|
|
96
|
-
def tokens(self) ->
|
|
100
|
+
def tokens(self) -> list[str]:
|
|
97
101
|
"""Find and return the tokens in the string.
|
|
98
102
|
|
|
99
103
|
:returns: The list of tokens in the string
|
|
@@ -108,7 +112,7 @@ class LocalizedString:
|
|
|
108
112
|
:raises Exception: If the language is not English
|
|
109
113
|
"""
|
|
110
114
|
if self.language != "en":
|
|
111
|
-
raise
|
|
115
|
+
raise DotStringsException(f"This should only be called for English strings: {self}")
|
|
112
116
|
return (
|
|
113
117
|
"NSLocalizedStringWithDefaultValue("
|
|
114
118
|
+ f'@"{self.key}", @"{self.table}", @"{self.bundle}", @"{self.value}", @"{self.comment}");'
|
|
@@ -186,11 +190,11 @@ class LocalizedString:
|
|
|
186
190
|
|
|
187
191
|
@staticmethod
|
|
188
192
|
def from_dotstring_entries(
|
|
189
|
-
*, entries:
|
|
190
|
-
) ->
|
|
193
|
+
*, entries: list[DotStringsEntry], language: str, table: str
|
|
194
|
+
) -> list["LocalizedString"]:
|
|
191
195
|
"""Convert a list of DotStringsEntry's into a list of LocalizedString's
|
|
192
196
|
|
|
193
|
-
:param
|
|
197
|
+
:param list[DotStringsEntry] entries: The DotStringsEntry's to convert
|
|
194
198
|
:param str language: The language the DotStringsEntry's are in
|
|
195
199
|
:param str table: The table the DotStringsEntry's are from
|
|
196
200
|
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import plistlib
|
|
4
4
|
import re
|
|
5
|
-
from typing import BinaryIO,
|
|
5
|
+
from typing import BinaryIO, Pattern, TextIO
|
|
6
6
|
|
|
7
|
+
from dotstrings.exceptions import DotStringsException
|
|
7
8
|
from dotstrings.dot_strings_entry import DotStringsEntry
|
|
8
9
|
from dotstrings.dot_stringsdict_entry import DotStringsDictEntry
|
|
9
10
|
|
|
@@ -13,7 +14,8 @@ class Patterns:
|
|
|
13
14
|
|
|
14
15
|
comment = re.compile(r"(\'(?:[^\'\\]|\\[\s\S])*\')|//.*|/\*(?:[^*]|\*(?!/))*\*/", re.MULTILINE)
|
|
15
16
|
whitespace = re.compile(r"\s*", re.MULTILINE)
|
|
16
|
-
entry = re.compile(r'"(.*)"
|
|
17
|
+
entry = re.compile(r'"(.*)"\s*=\s*"(.*)";')
|
|
18
|
+
quoteless_key_entry = re.compile(r'(.*?)\s*=\s*"(.*)";')
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class Scanner:
|
|
@@ -33,7 +35,7 @@ class Scanner:
|
|
|
33
35
|
"""
|
|
34
36
|
return self.offset < len(self.string)
|
|
35
37
|
|
|
36
|
-
def scan(self, pattern:
|
|
38
|
+
def scan(self, pattern: str | Pattern, flags: int = 0) -> str | None:
|
|
37
39
|
"""Scan a string for a pattern and return the string if found.
|
|
38
40
|
|
|
39
41
|
:param pattern: The pattern to scan for
|
|
@@ -56,7 +58,7 @@ class Scanner:
|
|
|
56
58
|
return None
|
|
57
59
|
|
|
58
60
|
|
|
59
|
-
def load(file_details:
|
|
61
|
+
def load(file_details: TextIO | str, encoding: str | None = None) -> list[DotStringsEntry]:
|
|
60
62
|
"""Parse the contents of a .strings file from a file pointer.
|
|
61
63
|
|
|
62
64
|
:param file_details: The file pointer or a file path
|
|
@@ -83,10 +85,10 @@ def load(file_details: Union[TextIO, str], encoding: Optional[str] = None) -> Li
|
|
|
83
85
|
except UnicodeDecodeError:
|
|
84
86
|
pass
|
|
85
87
|
|
|
86
|
-
raise
|
|
88
|
+
raise DotStringsException(f"Could not determine encoding for file at path: {file_details}")
|
|
87
89
|
|
|
88
90
|
|
|
89
|
-
def loads(contents: str) ->
|
|
91
|
+
def loads(contents: str) -> list[DotStringsEntry]:
|
|
90
92
|
"""Parse the contents of a .strings file.
|
|
91
93
|
|
|
92
94
|
Note: CRLF is not supported in strings.
|
|
@@ -98,7 +100,7 @@ def loads(contents: str) -> List[DotStringsEntry]:
|
|
|
98
100
|
# Sometimes we have CRLF. It's easier to just replace now. This could, in
|
|
99
101
|
# theory, cause issues, but we just don't support it for now.
|
|
100
102
|
if "\r\n" in contents:
|
|
101
|
-
raise
|
|
103
|
+
raise DotStringsException("Strings contain CRLF")
|
|
102
104
|
contents = contents.replace("\r\n", "\n")
|
|
103
105
|
|
|
104
106
|
scanner = Scanner(contents)
|
|
@@ -136,18 +138,32 @@ def loads(contents: str) -> List[DotStringsEntry]:
|
|
|
136
138
|
# Pull out any whitespace
|
|
137
139
|
_ = scanner.scan(Patterns.whitespace)
|
|
138
140
|
|
|
139
|
-
# Get the entry line
|
|
141
|
+
# Get the entry line. Always try with quotes first to avoid matching
|
|
142
|
+
# the "quoteless" style and then including the quotes in the key.
|
|
140
143
|
entry = scanner.scan(Patterns.entry)
|
|
144
|
+
regular_entry = True
|
|
141
145
|
|
|
142
146
|
if entry is None:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
entry = scanner.scan(Patterns.quoteless_key_entry)
|
|
148
|
+
regular_entry = False
|
|
149
|
+
|
|
150
|
+
if entry is None:
|
|
151
|
+
if scanner.has_more():
|
|
152
|
+
raise DotStringsException(
|
|
153
|
+
f"Expected an entry at offset {scanner.offset}"
|
|
154
|
+
)
|
|
155
|
+
break
|
|
146
156
|
|
|
147
157
|
# Now extract the key and value
|
|
148
|
-
|
|
158
|
+
if regular_entry:
|
|
159
|
+
entry_matches = Patterns.entry.search(entry)
|
|
160
|
+
else:
|
|
161
|
+
entry_matches = Patterns.quoteless_key_entry.search(entry)
|
|
162
|
+
|
|
149
163
|
if not entry_matches:
|
|
150
|
-
raise
|
|
164
|
+
raise DotStringsException(
|
|
165
|
+
f"Failed to parse entry at offset {scanner.offset}"
|
|
166
|
+
)
|
|
151
167
|
|
|
152
168
|
key = entry_matches.group(1)
|
|
153
169
|
value = entry_matches.group(2)
|
|
@@ -157,7 +173,7 @@ def loads(contents: str) -> List[DotStringsEntry]:
|
|
|
157
173
|
return strings
|
|
158
174
|
|
|
159
175
|
|
|
160
|
-
def load_dict(file_details:
|
|
176
|
+
def load_dict(file_details: BinaryIO | str) -> list[DotStringsDictEntry]:
|
|
161
177
|
"""Parse the contents of a .stringsdict file from a file pointer.
|
|
162
178
|
|
|
163
179
|
:param file_details: The file pointer or a file path
|
|
@@ -174,7 +190,7 @@ def load_dict(file_details: Union[BinaryIO, str]) -> List[DotStringsDictEntry]:
|
|
|
174
190
|
return load_dict(stringsdict_file)
|
|
175
191
|
|
|
176
192
|
|
|
177
|
-
def loads_dict(contents: bytes) ->
|
|
193
|
+
def loads_dict(contents: bytes) -> list[DotStringsDictEntry]:
|
|
178
194
|
"""Parse the contents of a .stringsdict file from binary data.
|
|
179
195
|
|
|
180
196
|
:param contents: The binary data of a .stringsdict file
|
|
@@ -185,12 +201,12 @@ def loads_dict(contents: bytes) -> List[DotStringsDictEntry]:
|
|
|
185
201
|
strings_dict = plistlib.loads(contents)
|
|
186
202
|
|
|
187
203
|
if not isinstance(strings_dict, dict):
|
|
188
|
-
raise
|
|
204
|
+
raise DotStringsException("stringsdict format is incorrect")
|
|
189
205
|
|
|
190
206
|
entries = []
|
|
191
207
|
for key, entry in strings_dict.items():
|
|
192
208
|
if not isinstance(entry, dict):
|
|
193
|
-
raise
|
|
209
|
+
raise DotStringsException("stringsdict entry format is incorrect")
|
|
194
210
|
|
|
195
211
|
entries.append(DotStringsDictEntry.parse(key, entry))
|
|
196
212
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "dotstrings"
|
|
3
|
-
version = "
|
|
3
|
+
version = "3.1.0"
|
|
4
4
|
description = "Tools for dealing with the .strings files for iOS and macOS"
|
|
5
5
|
|
|
6
6
|
license = "MIT"
|
|
7
7
|
|
|
8
8
|
authors = [
|
|
9
|
-
"Dale Myers <
|
|
9
|
+
"Dale Myers <dalemyers@microsoft.com>"
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
readme = 'README.md'
|
|
@@ -31,14 +31,14 @@ classifiers = [
|
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[tool.poetry.dependencies]
|
|
34
|
-
python = "^3.
|
|
34
|
+
python = "^3.10"
|
|
35
35
|
|
|
36
36
|
[tool.poetry.dev-dependencies]
|
|
37
|
-
black = "
|
|
38
|
-
mypy = "
|
|
39
|
-
pylint = "
|
|
40
|
-
pytest = "^
|
|
41
|
-
pytest-cov = "^
|
|
37
|
+
black = "24.4.0"
|
|
38
|
+
mypy = "1.9.0"
|
|
39
|
+
pylint = "3.1.0"
|
|
40
|
+
pytest = "^8.1.0"
|
|
41
|
+
pytest-cov = "^5.0.0"
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
[build-system]
|
|
File without changes
|
|
File without changes
|