bcsfe 3.0.0__py3-none-any.whl

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.
Files changed (168) hide show
  1. bcsfe/__init__.py +12 -0
  2. bcsfe/__main__.py +64 -0
  3. bcsfe/cli/__init__.py +21 -0
  4. bcsfe/cli/color.py +189 -0
  5. bcsfe/cli/dialog_creator.py +682 -0
  6. bcsfe/cli/edits/__init__.py +23 -0
  7. bcsfe/cli/edits/aku_realm.py +11 -0
  8. bcsfe/cli/edits/basic_items.py +402 -0
  9. bcsfe/cli/edits/cat_editor.py +715 -0
  10. bcsfe/cli/edits/clear_tutorial.py +11 -0
  11. bcsfe/cli/edits/enemy_editor.py +192 -0
  12. bcsfe/cli/edits/event_tickets.py +147 -0
  13. bcsfe/cli/edits/fixes.py +43 -0
  14. bcsfe/cli/edits/map.py +249 -0
  15. bcsfe/cli/edits/rare_ticket_trade.py +45 -0
  16. bcsfe/cli/feature_handler.py +294 -0
  17. bcsfe/cli/file_dialog.py +154 -0
  18. bcsfe/cli/main.py +327 -0
  19. bcsfe/cli/save_management.py +529 -0
  20. bcsfe/cli/server_cli.py +71 -0
  21. bcsfe/core/__init__.py +361 -0
  22. bcsfe/core/country_code.py +104 -0
  23. bcsfe/core/crypto.py +164 -0
  24. bcsfe/core/game/__init__.py +3 -0
  25. bcsfe/core/game/battle/__init__.py +3 -0
  26. bcsfe/core/game/battle/battle_items.py +127 -0
  27. bcsfe/core/game/battle/cleared_slots.py +306 -0
  28. bcsfe/core/game/battle/enemy.py +132 -0
  29. bcsfe/core/game/battle/slots.py +200 -0
  30. bcsfe/core/game/catbase/__init__.py +49 -0
  31. bcsfe/core/game/catbase/beacon_base.py +67 -0
  32. bcsfe/core/game/catbase/cat.py +1243 -0
  33. bcsfe/core/game/catbase/drop_chara.py +74 -0
  34. bcsfe/core/game/catbase/gatya.py +454 -0
  35. bcsfe/core/game/catbase/gatya_item.py +155 -0
  36. bcsfe/core/game/catbase/item_pack.py +190 -0
  37. bcsfe/core/game/catbase/login_bonuses.py +184 -0
  38. bcsfe/core/game/catbase/matatabi.py +69 -0
  39. bcsfe/core/game/catbase/medals.py +171 -0
  40. bcsfe/core/game/catbase/mission.py +321 -0
  41. bcsfe/core/game/catbase/my_sale.py +58 -0
  42. bcsfe/core/game/catbase/nyanko_club.py +254 -0
  43. bcsfe/core/game/catbase/officer_pass.py +79 -0
  44. bcsfe/core/game/catbase/playtime.py +97 -0
  45. bcsfe/core/game/catbase/powerup.py +179 -0
  46. bcsfe/core/game/catbase/scheme_items.py +195 -0
  47. bcsfe/core/game/catbase/special_skill.py +331 -0
  48. bcsfe/core/game/catbase/stamp.py +58 -0
  49. bcsfe/core/game/catbase/talent_orbs.py +710 -0
  50. bcsfe/core/game/catbase/unlock_popups.py +71 -0
  51. bcsfe/core/game/catbase/upgrade.py +190 -0
  52. bcsfe/core/game/catbase/user_rank_rewards.py +276 -0
  53. bcsfe/core/game/gamoto/__init__.py +9 -0
  54. bcsfe/core/game/gamoto/base_materials.py +90 -0
  55. bcsfe/core/game/gamoto/cat_shrine.py +207 -0
  56. bcsfe/core/game/gamoto/catamins.py +59 -0
  57. bcsfe/core/game/gamoto/gamatoto.py +496 -0
  58. bcsfe/core/game/gamoto/ototo.py +660 -0
  59. bcsfe/core/game/localizable.py +31 -0
  60. bcsfe/core/game/map/__init__.py +41 -0
  61. bcsfe/core/game/map/aku.py +229 -0
  62. bcsfe/core/game/map/challenge.py +73 -0
  63. bcsfe/core/game/map/chapters.py +362 -0
  64. bcsfe/core/game/map/dojo.py +360 -0
  65. bcsfe/core/game/map/enigma.py +219 -0
  66. bcsfe/core/game/map/event.py +898 -0
  67. bcsfe/core/game/map/ex_stage.py +102 -0
  68. bcsfe/core/game/map/gauntlets.py +351 -0
  69. bcsfe/core/game/map/item_reward_stage.py +416 -0
  70. bcsfe/core/game/map/legend_quest.py +381 -0
  71. bcsfe/core/game/map/map_names.py +100 -0
  72. bcsfe/core/game/map/map_reset.py +116 -0
  73. bcsfe/core/game/map/outbreaks.py +264 -0
  74. bcsfe/core/game/map/story.py +1166 -0
  75. bcsfe/core/game/map/timed_score.py +193 -0
  76. bcsfe/core/game/map/tower.py +68 -0
  77. bcsfe/core/game/map/uncanny.py +47 -0
  78. bcsfe/core/game/map/zero_legends.py +295 -0
  79. bcsfe/core/game_version.py +244 -0
  80. bcsfe/core/io/__init__.py +31 -0
  81. bcsfe/core/io/adb_handler.py +181 -0
  82. bcsfe/core/io/bc_csv.py +263 -0
  83. bcsfe/core/io/command.py +50 -0
  84. bcsfe/core/io/config.py +449 -0
  85. bcsfe/core/io/data.py +647 -0
  86. bcsfe/core/io/git_handler.py +92 -0
  87. bcsfe/core/io/json_file.py +45 -0
  88. bcsfe/core/io/path.py +269 -0
  89. bcsfe/core/io/root_handler.py +105 -0
  90. bcsfe/core/io/save.py +3730 -0
  91. bcsfe/core/io/thread_helper.py +76 -0
  92. bcsfe/core/io/waydroid.py +120 -0
  93. bcsfe/core/io/yaml.py +61 -0
  94. bcsfe/core/locale_handler.py +713 -0
  95. bcsfe/core/log.py +133 -0
  96. bcsfe/core/max_value_helper.py +35 -0
  97. bcsfe/core/server/__init__.py +21 -0
  98. bcsfe/core/server/client_info.py +34 -0
  99. bcsfe/core/server/event_data.py +377 -0
  100. bcsfe/core/server/game_data_getter.py +217 -0
  101. bcsfe/core/server/headers.py +29 -0
  102. bcsfe/core/server/managed_item.py +203 -0
  103. bcsfe/core/server/request.py +64 -0
  104. bcsfe/core/server/server_handler.py +668 -0
  105. bcsfe/core/server/updater.py +86 -0
  106. bcsfe/core/theme_handler.py +308 -0
  107. bcsfe/files/locales/en/core/config.properties +94 -0
  108. bcsfe/files/locales/en/core/files.properties +9 -0
  109. bcsfe/files/locales/en/core/input.properties +29 -0
  110. bcsfe/files/locales/en/core/locale.properties +33 -0
  111. bcsfe/files/locales/en/core/main.properties +108 -0
  112. bcsfe/files/locales/en/core/save.properties +97 -0
  113. bcsfe/files/locales/en/core/server.properties +47 -0
  114. bcsfe/files/locales/en/core/theme.properties +24 -0
  115. bcsfe/files/locales/en/core/updater.properties +19 -0
  116. bcsfe/files/locales/en/edits/bannable_items.properties +30 -0
  117. bcsfe/files/locales/en/edits/cats.properties +118 -0
  118. bcsfe/files/locales/en/edits/enemy.properties +17 -0
  119. bcsfe/files/locales/en/edits/fixes.properties +13 -0
  120. bcsfe/files/locales/en/edits/gamototo.properties +54 -0
  121. bcsfe/files/locales/en/edits/gatya.properties +5 -0
  122. bcsfe/files/locales/en/edits/gold_pass.properties +6 -0
  123. bcsfe/files/locales/en/edits/items.properties +52 -0
  124. bcsfe/files/locales/en/edits/map.properties +122 -0
  125. bcsfe/files/locales/en/edits/medals.properties +8 -0
  126. bcsfe/files/locales/en/edits/missions.properties +7 -0
  127. bcsfe/files/locales/en/edits/playtime.properties +7 -0
  128. bcsfe/files/locales/en/edits/scheme_items.properties +6 -0
  129. bcsfe/files/locales/en/edits/special_skills.properties +10 -0
  130. bcsfe/files/locales/en/edits/talent_orbs.properties +26 -0
  131. bcsfe/files/locales/en/edits/treasures.properties +24 -0
  132. bcsfe/files/locales/en/edits/user_rank.properties +9 -0
  133. bcsfe/files/locales/vi/core/config.properties +84 -0
  134. bcsfe/files/locales/vi/core/files.properties +9 -0
  135. bcsfe/files/locales/vi/core/input.properties +28 -0
  136. bcsfe/files/locales/vi/core/locale.properties +29 -0
  137. bcsfe/files/locales/vi/core/main.properties +108 -0
  138. bcsfe/files/locales/vi/core/save.properties +99 -0
  139. bcsfe/files/locales/vi/core/server.properties +67 -0
  140. bcsfe/files/locales/vi/core/theme.properties +23 -0
  141. bcsfe/files/locales/vi/core/updater.properties +21 -0
  142. bcsfe/files/locales/vi/edits/bannable_items.properties +30 -0
  143. bcsfe/files/locales/vi/edits/cats.properties +125 -0
  144. bcsfe/files/locales/vi/edits/enemy.properties +17 -0
  145. bcsfe/files/locales/vi/edits/fixes.properties +13 -0
  146. bcsfe/files/locales/vi/edits/gamototo.properties +54 -0
  147. bcsfe/files/locales/vi/edits/gatya.properties +4 -0
  148. bcsfe/files/locales/vi/edits/gold_pass.properties +6 -0
  149. bcsfe/files/locales/vi/edits/items.properties +51 -0
  150. bcsfe/files/locales/vi/edits/map.properties +118 -0
  151. bcsfe/files/locales/vi/edits/medals.properties +8 -0
  152. bcsfe/files/locales/vi/edits/missions.properties +7 -0
  153. bcsfe/files/locales/vi/edits/playtime.properties +7 -0
  154. bcsfe/files/locales/vi/edits/scheme_items.properties +6 -0
  155. bcsfe/files/locales/vi/edits/special_skills.properties +10 -0
  156. bcsfe/files/locales/vi/edits/talent_orbs.properties +25 -0
  157. bcsfe/files/locales/vi/edits/treasures.properties +24 -0
  158. bcsfe/files/locales/vi/edits/user_rank.properties +9 -0
  159. bcsfe/files/locales/vi/metadata.json +4 -0
  160. bcsfe/files/max_values.json +26 -0
  161. bcsfe/files/themes/default.json +16 -0
  162. bcsfe/py.typed +0 -0
  163. bcsfe-3.0.0.dist-info/METADATA +393 -0
  164. bcsfe-3.0.0.dist-info/RECORD +168 -0
  165. bcsfe-3.0.0.dist-info/WHEEL +5 -0
  166. bcsfe-3.0.0.dist-info/entry_points.txt +2 -0
  167. bcsfe-3.0.0.dist-info/licenses/LICENSE +675 -0
  168. bcsfe-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,713 @@
1
+ from __future__ import annotations
2
+ import dataclasses
3
+ import tempfile
4
+ from typing import Any
5
+ from bcsfe import core
6
+ from bcsfe.cli import color
7
+
8
+
9
+ class PropertySet:
10
+ """Represents a set of properties in a property file."""
11
+
12
+ def __init__(self, locale: str, property: str):
13
+ """Initializes a new instance of the PropertySet class.
14
+
15
+ Args:
16
+ locale (str): Language code of the locale.
17
+ property (str): Name of the property file.
18
+ """
19
+ self.locale = locale
20
+ self.property = property
21
+ self.path = LocalManager.get_locale_folder(locale).add(property + ".properties")
22
+ self.properties: dict[str, tuple[str, str]] = {}
23
+ self.parse()
24
+
25
+ def parse(self):
26
+ """Parses the property file.
27
+
28
+ Raises:
29
+ KeyError: If a key is already defined in the property file.
30
+ """
31
+ lines = self.path.read().to_str().splitlines()
32
+ i = 0
33
+ in_multi_line = False
34
+ multi_line_text = ""
35
+ multi_line_key = ""
36
+
37
+ while i < len(lines):
38
+ line = lines[i]
39
+ finish_multiline = False
40
+ if (in_multi_line and not line.startswith(">")) or (
41
+ in_multi_line and i == len(lines) - 1
42
+ ):
43
+ in_multi_line = False
44
+ finish_multiline = True
45
+ if multi_line_key in self.properties:
46
+ raise KeyError(
47
+ f"Key {multi_line_key} already exists in property file"
48
+ )
49
+ if line.startswith(">"):
50
+ multi_line_text += line[1:]
51
+ else:
52
+ multi_line_text = multi_line_text[:-1] # remove extra newline
53
+ self.properties[multi_line_key] = (multi_line_text, self.property)
54
+ multi_line_text = ""
55
+ multi_line_key = ""
56
+ if line.startswith("#") or not line:
57
+ i += 1
58
+ continue
59
+ if line.startswith(">") and in_multi_line:
60
+ multi_line_text += line[1:] + "\n"
61
+
62
+ parts = line.split("=")
63
+ if line.strip().endswith("="):
64
+ in_multi_line = True
65
+ multi_line_key = parts[0]
66
+
67
+ if not in_multi_line and not finish_multiline:
68
+ key = parts[0]
69
+ value = "=".join(parts[1:])
70
+ if key in self.properties:
71
+ raise KeyError(f"Key {key} already exists in property file")
72
+ self.properties[key] = (value, self.property)
73
+
74
+ i += 1
75
+
76
+ def get_key(self, key: str) -> str:
77
+ """Gets a key from the property file.
78
+
79
+ Args:
80
+ key (str): Key to get.
81
+
82
+ Returns:
83
+ str: Value of the key.
84
+ """
85
+ return (
86
+ self.properties.get(key, key)[0].replace("\\n", "\n").replace("\\t", "\t")
87
+ )
88
+
89
+ @staticmethod
90
+ def from_config(property: str) -> PropertySet:
91
+ """Gets a PropertySet from the language code in the config.
92
+
93
+ Args:
94
+ property (str): Name of the property file.
95
+
96
+ Returns:
97
+ PropertySet: PropertySet for the property file.
98
+ """
99
+ return PropertySet(
100
+ core.core_data.config.get_str(core.ConfigKey.LOCALE), property
101
+ )
102
+
103
+
104
+ class LocalManager:
105
+ """Manages properties for a locale"""
106
+
107
+ def __init__(self, locale: str | None = None):
108
+ """Initializes a new instance of the LocalManager class.
109
+
110
+ Args:
111
+ locale (str): Language code of the locale.
112
+ """
113
+ if locale is None:
114
+ lc = core.core_data.config.get_str(core.ConfigKey.LOCALE)
115
+ else:
116
+ lc = locale
117
+
118
+ self.locale = lc
119
+ self.path = LocalManager.get_locale_folder(lc)
120
+ self.properties: dict[str, PropertySet] = {}
121
+ self.all_properties: dict[str, tuple[str, str]] = {}
122
+ self.en_properties: dict[str, tuple[str, str]] = {}
123
+ self.en_properties_path = LocalManager.get_locale_folder("en")
124
+ self.authors: list[str] = ["fieryhenry"]
125
+ self.name: str = "English"
126
+ self.parse()
127
+ if self.locale == "en":
128
+ self.en_properties = self.all_properties
129
+
130
+ if core.core_data.config.get_bool(core.ConfigKey.SHOW_MISSING_LOCALE_KEYS):
131
+ key = self.get_key("missing_locale_keys")
132
+ print(key)
133
+ print()
134
+ missing = self.get_missing_keys()
135
+ for key in missing:
136
+ print(key)
137
+ if not missing:
138
+ print(self.get_key("none"))
139
+
140
+ print()
141
+
142
+ key = self.get_key("extra_locale_keys")
143
+ print(key)
144
+ print()
145
+ extra = self.get_extra_keys()
146
+ for key in extra:
147
+ print(key)
148
+ if not extra:
149
+ print(self.get_key("none"))
150
+
151
+ print()
152
+
153
+ def get_missing_keys(self) -> list[tuple[str, str, str]]:
154
+ missing = set(self.en_properties.keys()) - set(self.all_properties.keys())
155
+
156
+ return [
157
+ (
158
+ key,
159
+ self.en_properties[key][0],
160
+ self.en_properties[key][1] + ".properties",
161
+ )
162
+ for key in missing
163
+ ]
164
+
165
+ def get_extra_keys(self) -> list[tuple[str, str, str]]:
166
+ extra = set(self.all_properties.keys()) - set(self.en_properties.keys())
167
+
168
+ return [
169
+ (
170
+ key,
171
+ self.all_properties[key][0],
172
+ self.all_properties[key][1] + ".properties",
173
+ )
174
+ for key in extra
175
+ ]
176
+
177
+ def parse(self):
178
+ """Parses all property files in the locale folder recursively."""
179
+ for file in self.path.glob("**/*.properties", recursive=True):
180
+ file_name = file.strip_path_from(self.path).path
181
+ property_set = PropertySet(self.locale, file_name[:-11])
182
+ self.all_properties.update(property_set.properties)
183
+ self.properties[file_name[:-11]] = property_set
184
+
185
+ metadata_path = self.path.add("metadata.json")
186
+
187
+ if metadata_path.exists():
188
+ data = core.JsonFile.from_path(metadata_path)
189
+ self.authors = data.get("authors") or ["fieryhenry"]
190
+ self.name = data.get("name") or "English"
191
+
192
+ if self.locale != "en":
193
+ for file in self.en_properties_path.glob("**/*.properties", recursive=True):
194
+ file_name = file.strip_path_from(self.en_properties_path).path
195
+ property_set = PropertySet("en", file_name[:-11])
196
+ self.en_properties.update(property_set.properties)
197
+
198
+ def get_key(self, key: str, escape: bool = True, **kwargs: Any) -> str:
199
+ """Gets a key from the property file.
200
+
201
+ Args:
202
+ key (str): Key to get.
203
+
204
+ Returns:
205
+ str: Value of the key.
206
+ """
207
+ try:
208
+ text = self.get_key_recursive(key, kwargs, escape)
209
+ except RecursionError:
210
+ text = key
211
+
212
+ for kwarg_key, kwarg_value in kwargs.items():
213
+ value = str(kwarg_value)
214
+ if escape:
215
+ value = LocalManager.escape_string(value)
216
+ text = text.replace("{" + kwarg_key + "}", value)
217
+
218
+ if "$(" in text:
219
+ text = self.parse_condition(text, kwargs)
220
+
221
+ return text
222
+
223
+ def parse_condition(self, text: str, kwargs: dict[str, Any]) -> str:
224
+ counter = 0
225
+ final_text = ""
226
+ in_expression = False
227
+ expression_text = ""
228
+ count_down = 0
229
+ while counter < len(text):
230
+ char = text[counter]
231
+ if counter == len(text) - 1:
232
+ final_text += char
233
+ break
234
+ next_char = text[counter + 1]
235
+ if char == "\\":
236
+ final_text += next_char
237
+ counter += 2
238
+ continue
239
+ if char == "$" and next_char == "(":
240
+ count_down = 0
241
+ in_expression = True
242
+ elif char == "/" and next_char == "$":
243
+ count_down = 2
244
+ in_expression = False
245
+ if len(expression_text) < 3:
246
+ counter += 1
247
+ continue
248
+ new_expression_text = expression_text[2:-1]
249
+ expression_text = ""
250
+ parts = new_expression_text.split(":")
251
+ if len(parts) < 2:
252
+ counter += 1
253
+ continue
254
+ keyword = parts[0].strip()
255
+ expression = parts[1].strip()
256
+ conditions = expression.split("$,")
257
+ string = ""
258
+ for i, condition in enumerate(conditions):
259
+ condition = condition.strip()
260
+ if not condition:
261
+ continue
262
+ if i == len(conditions) - 1:
263
+ string = condition
264
+ break
265
+ condition_parts = condition.split("($")
266
+ if len(condition_parts) < 2:
267
+ continue
268
+ logic = condition_parts[0].strip()
269
+ word = condition_parts[1].strip()
270
+ if not word:
271
+ continue
272
+ word = word[:-1]
273
+ value = kwargs.get(keyword)
274
+ if value is None:
275
+ continue
276
+ equality = None
277
+ if logic.startswith("=="):
278
+ equality = "=="
279
+ elif logic.startswith("!="):
280
+ equality = "!="
281
+ elif logic.startswith(">="):
282
+ equality = ">="
283
+ elif logic.startswith("<="):
284
+ equality = "<="
285
+ elif logic.startswith(">"):
286
+ equality = ">"
287
+ elif logic.startswith("<"):
288
+ equality = "<"
289
+ if equality is None:
290
+ continue
291
+ logic_parts = logic.split(equality)
292
+ if len(logic_parts) < 2:
293
+ continue
294
+ logic_value = logic_parts[1].strip()
295
+
296
+ if isinstance(value, int):
297
+ if not logic_value.isdigit():
298
+ continue
299
+ logic_value = int(logic_value)
300
+
301
+ if equality == "==":
302
+ if logic_value == value:
303
+ string = word
304
+ break
305
+ elif equality == "!=":
306
+ if logic_value != value:
307
+ string = word
308
+ break
309
+
310
+ if isinstance(logic_value, int) and not string:
311
+ if equality == ">":
312
+ if logic_value > value:
313
+ string = word
314
+ break
315
+ elif equality == ">=":
316
+ if logic_value >= value:
317
+ string = word
318
+ break
319
+ elif equality == "<":
320
+ if logic_value < value:
321
+ string = word
322
+ break
323
+ elif equality == "<=":
324
+ if logic_value <= value:
325
+ string = word
326
+ break
327
+
328
+ final_text += string
329
+
330
+ if in_expression:
331
+ expression_text += char
332
+ else:
333
+ if count_down <= 0:
334
+ final_text += char
335
+ else:
336
+ count_down -= 1
337
+
338
+ counter += 1
339
+
340
+ return final_text
341
+
342
+ @staticmethod
343
+ def get_special_chars() -> list[str]:
344
+ return ["<", ">", "/"]
345
+
346
+ @staticmethod
347
+ def escape_string(string: str) -> str:
348
+ for char in LocalManager.get_special_chars():
349
+ string = string.replace(char, "\\" + char)
350
+ return string
351
+
352
+ def get_key_recursive(
353
+ self,
354
+ key: str,
355
+ kwargs: dict[str, Any],
356
+ escape: bool = True,
357
+ ) -> str:
358
+ value = self.all_properties.get(key)
359
+ if value is None:
360
+ value = self.en_properties.get(key, (key, key))
361
+ value = value[0].replace("\\n", "\n").replace("\\t", "\t")
362
+ # replace {{key}} with the value of the key
363
+ if "{{" not in value:
364
+ return value
365
+ char_index = 0
366
+ while char_index < len(value):
367
+ if value[char_index] == "{" and value[char_index + 1] == "{":
368
+ key_name = ""
369
+ char_index += 2
370
+ while value[char_index] != "}":
371
+ key_name += value[char_index]
372
+ char_index += 1
373
+
374
+ if key_name != key:
375
+ value = value.replace(
376
+ "{{" + key_name + "}}",
377
+ self.get_key(key_name, escape, **kwargs),
378
+ )
379
+ char_index += 1
380
+
381
+ return value
382
+
383
+ @staticmethod
384
+ def get_all_aliases(value: str) -> list[str]:
385
+ """Gets all aliases from a string. Aliases are separated by |.
386
+
387
+ Args:
388
+ value (str): String to get aliases from.
389
+
390
+ Returns:
391
+ list[str]: List of aliases.
392
+ """
393
+ if "|" not in value:
394
+ return [value]
395
+ i = 0
396
+ aliases: list[str] = []
397
+ while i < len(value):
398
+ char = value[i]
399
+ prev_char = value[i - 1] if i > 0 else ""
400
+ if char == "|" and prev_char != "\\":
401
+ aliases.append(value[:i])
402
+ value = value[i + 1 :]
403
+ i = 0
404
+ i += 1
405
+
406
+ aliases.append(value)
407
+ return aliases
408
+
409
+ @staticmethod
410
+ def from_config() -> LocalManager:
411
+ """Gets a LocalManager from the language code in the config.
412
+
413
+ Returns:
414
+ LocalManager: LocalManager for the locale.
415
+ """
416
+ return LocalManager(core.core_data.config.get_str(core.ConfigKey.LOCALE))
417
+
418
+ def check_duplicates(self):
419
+ """Checks for duplicate keys in all property files.
420
+
421
+ Raises:
422
+ KeyError: If a key is already defined in the property file.
423
+ """
424
+ keys: set[str] = set()
425
+ for property in self.properties.values():
426
+ for key in property.properties.keys():
427
+ if key in keys:
428
+ raise KeyError(f"Duplicate key {key}")
429
+ keys.add(key)
430
+
431
+ @staticmethod
432
+ def get_all_locales() -> list[str]:
433
+ """Gets all locales in the locales folder.
434
+
435
+ Returns:
436
+ list[str]: List of locales.
437
+ """
438
+ locales: list[str] = []
439
+ for folder in LocalManager.get_locales_folder().get_dirs():
440
+ locales.append(folder.basename())
441
+ for folder in LocalManager.get_external_locales_folder().get_dirs():
442
+ locales.append(folder.basename())
443
+ return locales
444
+
445
+ @staticmethod
446
+ def get_locales_folder() -> core.Path:
447
+ """Gets the locales folder.
448
+
449
+ Returns:
450
+ core.Path: Path to the locales folder.
451
+ """
452
+ return core.Path("locales", True)
453
+
454
+ @staticmethod
455
+ def get_external_locales_folder() -> core.Path:
456
+ """Gets the external locales folder.
457
+
458
+ Returns:
459
+ core.Path: Path to the external locales folder.
460
+ """
461
+ return core.Path.get_documents_folder().add("external_locales")
462
+
463
+ @staticmethod
464
+ def get_locale_folder(locale: str) -> core.Path:
465
+ """Gets the folder for a locale.
466
+
467
+ Args:
468
+ locale (str): Language code of the locale.
469
+
470
+ Returns:
471
+ core.Path: Path to the locale folder.
472
+ """
473
+ if locale.startswith("ext-"):
474
+ return LocalManager.get_external_locales_folder().add(locale)
475
+ return LocalManager.get_locales_folder().add(locale)
476
+
477
+ @staticmethod
478
+ def remove_locale(locale: str):
479
+ """Removes a locale.
480
+
481
+ Args:
482
+ locale (str): Language code of the locale.
483
+ """
484
+ if locale not in LocalManager.get_all_locales():
485
+ return
486
+ if locale.startswith("ext-"):
487
+ extern = ExternalLocaleManager.get_external_locale(locale)
488
+ if extern is not None:
489
+ ExternalLocaleManager.delete_locale(extern)
490
+ LocalManager.get_external_locales_folder().add(locale).remove()
491
+ else:
492
+ LocalManager.get_locales_folder().add(locale).remove()
493
+
494
+ if core.core_data.config.get_str(core.ConfigKey.LOCALE) == locale:
495
+ core.core_data.config.set(core.ConfigKey.LOCALE, "en")
496
+
497
+
498
+ @dataclasses.dataclass
499
+ class ExternalLocale:
500
+ short_name: str
501
+ name: str
502
+ description: str
503
+ author: str
504
+ version: str
505
+ git_repo: str | None = None
506
+
507
+ def to_json(self) -> dict[str, Any]:
508
+ return dataclasses.asdict(self)
509
+
510
+ @staticmethod
511
+ def from_json(json_data: dict[str, Any]) -> ExternalLocale | None:
512
+ short_name = json_data.get("short_name")
513
+ name = json_data.get("name")
514
+ description = json_data.get("description")
515
+ author = json_data.get("author")
516
+ version = json_data.get("version")
517
+ git_repo = json_data.get("git_repo")
518
+ if (
519
+ short_name is None
520
+ or name is None
521
+ or description is None
522
+ or author is None
523
+ or version is None
524
+ ):
525
+ return None
526
+ return ExternalLocale(
527
+ short_name,
528
+ name,
529
+ description,
530
+ author,
531
+ version,
532
+ git_repo,
533
+ )
534
+
535
+ @staticmethod
536
+ def from_git_repo(git_repo: str) -> ExternalLocale | None:
537
+ repo = core.GitHandler().get_repo(git_repo)
538
+ if repo is None:
539
+ return None
540
+ locale_json = repo.get_file(core.Path("locale.json"))
541
+ if locale_json is None:
542
+ return None
543
+ json_data = core.JsonFile.from_data(locale_json).to_object()
544
+ json_data["git_repo"] = git_repo
545
+ return ExternalLocale.from_json(json_data)
546
+
547
+ def get_new_version(self) -> bool:
548
+ if self.git_repo is None:
549
+ return False
550
+ repo = core.GitHandler().get_repo(self.git_repo)
551
+ if repo is None:
552
+ return False
553
+ with tempfile.TemporaryDirectory() as tmp:
554
+ temp_dir = core.Path(tmp)
555
+ success = repo.clone_to_temp(temp_dir)
556
+ if not success:
557
+ return False
558
+ external_locale = ExternalLocaleManager.parse_external_locale(temp_dir)
559
+ if external_locale is None:
560
+ return False
561
+ version = external_locale.version
562
+
563
+ if version == self.version:
564
+ return False
565
+
566
+ self.name = external_locale.name
567
+ self.short_name = external_locale.short_name
568
+ self.description = external_locale.description
569
+ self.author = external_locale.author
570
+ self.version = version
571
+
572
+ success = repo.pull()
573
+ if not success:
574
+ return False
575
+ self.save()
576
+ return True
577
+
578
+ def save(self):
579
+ ExternalLocaleManager.save_locale(self)
580
+
581
+ def get_full_name(self) -> str:
582
+ return f"ext-{self.author}-{self.short_name}"
583
+
584
+
585
+ class ExternalLocaleManager:
586
+ @staticmethod
587
+ def delete_locale(external_locale: ExternalLocale):
588
+ if external_locale.git_repo is None:
589
+ return
590
+ folder = core.GitHandler.get_repo_folder().add(
591
+ external_locale.git_repo.split("/")[-1]
592
+ )
593
+ folder.remove()
594
+
595
+ @staticmethod
596
+ def save_locale(
597
+ external_locale: ExternalLocale,
598
+ ):
599
+ """Saves an external locale.
600
+
601
+ Args:
602
+ external_locale (ExternalLocale): External locale to save.
603
+ """
604
+ if external_locale.git_repo is None:
605
+ return
606
+ folder = LocalManager.get_external_locales_folder().add(
607
+ external_locale.get_full_name()
608
+ )
609
+ folder.generate_dirs()
610
+
611
+ repo = core.GitHandler().get_repo(external_locale.git_repo)
612
+ if repo is None:
613
+ return
614
+ files_dir = repo.get_folder(core.Path("files"))
615
+ if files_dir is None:
616
+ return
617
+
618
+ files_dir.copy_tree(folder)
619
+
620
+ json_data = external_locale.to_json()
621
+ folder.add("locale.json").write(core.JsonFile.from_object(json_data).to_data())
622
+
623
+ @staticmethod
624
+ def parse_external_locale(path: core.Path) -> ExternalLocale | None:
625
+ """Parses an external locale.
626
+
627
+ Args:
628
+ path (core.Path): Path to the external locale.
629
+
630
+ Returns:
631
+ ExternalLocale: External locale.
632
+ """
633
+ if not path.exists():
634
+ return None
635
+ json_data = core.JsonFile.from_data(path.add("locale.json").read()).to_object()
636
+ return ExternalLocale.from_json(json_data)
637
+
638
+ @staticmethod
639
+ def update_external_locale(external_locale: ExternalLocale):
640
+ """Updates an external locale.
641
+
642
+ Args:
643
+ external_locale (ExternalLocale): External locale to update.
644
+ """
645
+ if external_locale.git_repo is None:
646
+ return
647
+ color.ColoredText.localize(
648
+ "checking_for_locale_updates",
649
+ locale_name=external_locale.name,
650
+ )
651
+ updated = external_locale.get_new_version()
652
+ if updated:
653
+ color.ColoredText.localize(
654
+ "external_locale_updated",
655
+ locale_name=external_locale.name,
656
+ version=external_locale.version,
657
+ )
658
+ else:
659
+ color.ColoredText.localize(
660
+ "external_locale_no_update",
661
+ locale_name=external_locale.name,
662
+ version=external_locale.version,
663
+ )
664
+ print()
665
+
666
+ @staticmethod
667
+ def update_all_external_locales(_: Any = None):
668
+ """Updates all external locales."""
669
+ dirs = LocalManager.get_external_locales_folder().get_dirs()
670
+ if not dirs:
671
+ color.ColoredText.localize(
672
+ "no_external_locales",
673
+ )
674
+ return
675
+ if not core.GitHandler.is_git_installed():
676
+ color.ColoredText.localize(
677
+ "git_not_installed",
678
+ )
679
+ return
680
+ for folder in dirs:
681
+ locale = ExternalLocaleManager.parse_external_locale(folder)
682
+ if locale is None:
683
+ continue
684
+ ExternalLocaleManager.update_external_locale(locale)
685
+
686
+ @staticmethod
687
+ def get_external_locale_config() -> ExternalLocale | None:
688
+ """Gets the external locale from the config.
689
+
690
+ Returns:
691
+ ExternalLocale: External locale.
692
+ """
693
+
694
+ locale = core.core_data.config.get_str(core.ConfigKey.LOCALE)
695
+ if not locale.startswith("ext-"):
696
+ return None
697
+ return ExternalLocaleManager.parse_external_locale(
698
+ LocalManager.get_locale_folder(locale)
699
+ )
700
+
701
+ @staticmethod
702
+ def get_external_locale(locale: str) -> ExternalLocale | None:
703
+ """Gets the external locale from the code.
704
+
705
+ Returns:
706
+ ExternalLocale: External locale.
707
+ """
708
+
709
+ if not locale.startswith("ext-"):
710
+ return None
711
+ return ExternalLocaleManager.parse_external_locale(
712
+ LocalManager.get_locale_folder(locale)
713
+ )