kerykeion 5.0.0b1__py3-none-any.whl → 5.0.0b4__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.

Potentially problematic release.


This version of kerykeion might be problematic. Click here for more details.

Files changed (30) hide show
  1. kerykeion/__init__.py +3 -2
  2. kerykeion/aspects/aspects_factory.py +60 -21
  3. kerykeion/aspects/aspects_utils.py +1 -1
  4. kerykeion/backword.py +111 -18
  5. kerykeion/chart_data_factory.py +72 -7
  6. kerykeion/charts/chart_drawer.py +601 -206
  7. kerykeion/charts/charts_utils.py +440 -255
  8. kerykeion/charts/templates/aspect_grid_only.xml +269 -312
  9. kerykeion/charts/templates/chart.xml +302 -328
  10. kerykeion/charts/templates/wheel_only.xml +271 -312
  11. kerykeion/charts/themes/black-and-white.css +148 -0
  12. kerykeion/relationship_score_factory.py +12 -2
  13. kerykeion/schemas/chart_template_model.py +27 -0
  14. kerykeion/schemas/kr_literals.py +1 -1
  15. kerykeion/settings/__init__.py +16 -2
  16. kerykeion/settings/chart_defaults.py +444 -0
  17. kerykeion/settings/config_constants.py +0 -5
  18. kerykeion/settings/kerykeion_settings.py +31 -74
  19. kerykeion/settings/translation_strings.py +1479 -0
  20. kerykeion/settings/translations.py +74 -0
  21. kerykeion/transits_time_range_factory.py +10 -1
  22. {kerykeion-5.0.0b1.dist-info → kerykeion-5.0.0b4.dist-info}/METADATA +304 -204
  23. {kerykeion-5.0.0b1.dist-info → kerykeion-5.0.0b4.dist-info}/RECORD +25 -26
  24. kerykeion/settings/kr.config.json +0 -1474
  25. kerykeion/settings/legacy/__init__.py +0 -0
  26. kerykeion/settings/legacy/legacy_celestial_points_settings.py +0 -299
  27. kerykeion/settings/legacy/legacy_chart_aspects_settings.py +0 -71
  28. kerykeion/settings/legacy/legacy_color_settings.py +0 -42
  29. {kerykeion-5.0.0b1.dist-info → kerykeion-5.0.0b4.dist-info}/WHEEL +0 -0
  30. {kerykeion-5.0.0b1.dist-info → kerykeion-5.0.0b4.dist-info}/licenses/LICENSE +0 -0
@@ -5,19 +5,23 @@
5
5
 
6
6
 
7
7
  import logging
8
- import swisseph as swe
9
- from typing import get_args, Union, Any
8
+ from copy import deepcopy
9
+ from math import ceil
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from string import Template
13
+ from typing import Any, Mapping, Optional, Union, get_args
10
14
 
15
+ import swisseph as swe
16
+ from scour.scour import scourString
11
17
 
12
- from kerykeion.schemas.kr_models import ChartDataModel
13
- from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
14
- from kerykeion.settings.kerykeion_settings import get_settings
15
18
  from kerykeion.house_comparison.house_comparison_factory import HouseComparisonFactory
16
19
  from kerykeion.schemas import (
17
20
  KerykeionException,
18
21
  ChartType,
19
22
  Sign,
20
23
  ActiveAspect,
24
+ KerykeionPointModel,
21
25
  )
22
26
  from kerykeion.schemas import ChartTemplateModel
23
27
  from kerykeion.schemas.kr_models import (
@@ -27,13 +31,17 @@ from kerykeion.schemas.kr_models import (
27
31
  )
28
32
  from kerykeion.schemas.settings_models import (
29
33
  KerykeionSettingsCelestialPointModel,
30
- KerykeionSettingsModel,
34
+ KerykeionLanguageModel,
31
35
  )
32
36
  from kerykeion.schemas.kr_literals import (
33
37
  KerykeionChartTheme,
34
38
  KerykeionChartLanguage,
35
39
  AstrologicalPoint,
36
40
  )
41
+ from kerykeion.schemas.kr_models import ChartDataModel
42
+ from kerykeion.settings import LANGUAGE_SETTINGS
43
+ from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
44
+ from kerykeion.settings.translations import get_translations, load_language_settings
37
45
  from kerykeion.charts.charts_utils import (
38
46
  draw_zodiac_slice,
39
47
  convert_latitude_coordinate_to_string,
@@ -62,14 +70,12 @@ from kerykeion.charts.charts_utils import (
62
70
  )
63
71
  from kerykeion.charts.draw_planets import draw_planets
64
72
  from kerykeion.utilities import get_houses_list, inline_css_variables_in_svg, distribute_percentages_to_100
65
- from kerykeion.settings.legacy.legacy_color_settings import DEFAULT_CHART_COLORS
66
- from kerykeion.settings.legacy.legacy_celestial_points_settings import DEFAULT_CELESTIAL_POINTS_SETTINGS
67
- from kerykeion.settings.legacy.legacy_chart_aspects_settings import DEFAULT_CHART_ASPECTS_SETTINGS
68
- from pathlib import Path
69
- from scour.scour import scourString
70
- from string import Template
73
+ from kerykeion.settings.chart_defaults import (
74
+ DEFAULT_CHART_COLORS,
75
+ DEFAULT_CELESTIAL_POINTS_SETTINGS,
76
+ DEFAULT_CHART_ASPECTS_SETTINGS,
77
+ )
71
78
  from typing import List, Literal
72
- from datetime import datetime
73
79
 
74
80
 
75
81
  class ChartDrawer:
@@ -98,14 +104,16 @@ class ChartDrawer:
98
104
  Pre-computed chart data from ChartDataFactory containing all subjects, aspects,
99
105
  element/quality distributions, and other analytical data. This is the ONLY source
100
106
  of chart information - no calculations are performed by ChartDrawer.
101
- new_settings_file (Path | dict | KerykeionSettingsModel, optional):
102
- Path or settings object to override default chart configuration (colors, fonts, aspects).
103
107
  theme (KerykeionChartTheme, optional):
104
108
  CSS theme for the chart. If None, no default styles are applied. Defaults to 'classic'.
105
109
  double_chart_aspect_grid_type (Literal['list', 'table'], optional):
106
110
  Specifies rendering style for double-chart aspect grids. Defaults to 'list'.
107
111
  chart_language (KerykeionChartLanguage, optional):
108
112
  Language code for chart labels. Defaults to 'EN'.
113
+ language_pack (dict | None, optional):
114
+ Additional translations merged over the bundled defaults for the
115
+ selected language. Useful to introduce new languages or override
116
+ existing labels.
109
117
  transparent_background (bool, optional):
110
118
  Whether to use a transparent background instead of the theme color. Defaults to False.
111
119
 
@@ -157,10 +165,29 @@ class ChartDrawer:
157
165
 
158
166
  _DEFAULT_HEIGHT = 550
159
167
  _DEFAULT_FULL_WIDTH = 1250
168
+ _DEFAULT_SYNASTRY_WIDTH = 1570
160
169
  _DEFAULT_NATAL_WIDTH = 870
161
170
  _DEFAULT_FULL_WIDTH_WITH_TABLE = 1250
162
171
  _DEFAULT_ULTRA_WIDE_WIDTH = 1320
163
172
 
173
+ _ASPECT_LIST_ASPECTS_PER_COLUMN = 14
174
+ _ASPECT_LIST_COLUMN_WIDTH = 105
175
+
176
+ _BASE_VERTICAL_OFFSETS = {
177
+ "wheel": 50,
178
+ "grid": 0,
179
+ "aspect_grid": 50,
180
+ "aspect_list": 50,
181
+ "title": 0,
182
+ "elements": 0,
183
+ "qualities": 0,
184
+ "lunar_phase": 518,
185
+ "bottom_left": 0,
186
+ }
187
+ _MAX_TOP_SHIFT = 80
188
+ _TOP_SHIFT_FACTOR = 2
189
+ _ROW_HEIGHT = 8
190
+
164
191
  _BASIC_CHART_VIEWBOX = f"0 0 {_DEFAULT_NATAL_WIDTH} {_DEFAULT_HEIGHT}"
165
192
  _WIDE_CHART_VIEWBOX = f"0 0 {_DEFAULT_FULL_WIDTH} 546.0"
166
193
  _ULTRA_WIDE_CHART_VIEWBOX = f"0 0 {_DEFAULT_ULTRA_WIDE_WIDTH} 546.0"
@@ -170,7 +197,6 @@ class ChartDrawer:
170
197
  first_obj: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel]
171
198
  second_obj: Union[AstrologicalSubjectModel, PlanetReturnModel, None]
172
199
  chart_type: ChartType
173
- new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
174
200
  theme: Union[KerykeionChartTheme, None]
175
201
  double_chart_aspect_grid_type: Literal["list", "table"]
176
202
  chart_language: KerykeionChartLanguage
@@ -179,6 +205,8 @@ class ChartDrawer:
179
205
  transparent_background: bool
180
206
  external_view: bool
181
207
  custom_title: Union[str, None]
208
+ _language_model: KerykeionLanguageModel
209
+ _fallback_language_model: KerykeionLanguageModel
182
210
 
183
211
  # Internal properties
184
212
  fire: float
@@ -204,10 +232,10 @@ class ChartDrawer:
204
232
  self,
205
233
  chart_data: "ChartDataModel",
206
234
  *,
207
- new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] = None,
208
235
  theme: Union[KerykeionChartTheme, None] = "classic",
209
236
  double_chart_aspect_grid_type: Literal["list", "table"] = "list",
210
237
  chart_language: KerykeionChartLanguage = "EN",
238
+ language_pack: Optional[Mapping[str, Any]] = None,
211
239
  external_view: bool = False,
212
240
  transparent_background: bool = False,
213
241
  colors_settings: dict = DEFAULT_CHART_COLORS,
@@ -224,14 +252,16 @@ class ChartDrawer:
224
252
  chart_data (ChartDataModel):
225
253
  Pre-computed chart data from ChartDataFactory containing all subjects,
226
254
  aspects, element/quality distributions, and other analytical data.
227
- new_settings_file (Path, dict, or KerykeionSettingsModel, optional):
228
- Custom settings source for chart colors, fonts, and aspects.
229
255
  theme (KerykeionChartTheme or None, optional):
230
256
  CSS theme to apply; None for default styling.
231
257
  double_chart_aspect_grid_type (Literal['list','table'], optional):
232
258
  Layout style for double-chart aspect grids ('list' or 'table').
233
259
  chart_language (KerykeionChartLanguage, optional):
234
260
  Language code for chart labels (e.g., 'EN', 'IT').
261
+ language_pack (dict | None, optional):
262
+ Additional translations merged over the bundled defaults for the
263
+ selected language. Useful to introduce new languages or override
264
+ existing labels.
235
265
  external_view (bool, optional):
236
266
  Whether to use external visualization (planets on outer ring) for single-subject charts. Defaults to False.
237
267
  transparent_background (bool, optional):
@@ -242,17 +272,17 @@ class ChartDrawer:
242
272
  # --------------------
243
273
  # COMMON INITIALIZATION
244
274
  # --------------------
245
- self.new_settings_file = new_settings_file
246
275
  self.chart_language = chart_language
247
276
  self.double_chart_aspect_grid_type = double_chart_aspect_grid_type
248
277
  self.transparent_background = transparent_background
249
278
  self.external_view = external_view
250
- self.chart_colors_settings = colors_settings
251
- self.planets_settings = celestial_points_settings
252
- self.aspects_settings = aspects_settings
279
+ self.chart_colors_settings = deepcopy(colors_settings)
280
+ self.planets_settings = [dict(body) for body in celestial_points_settings]
281
+ self.aspects_settings = [dict(aspect) for aspect in aspects_settings]
253
282
  self.custom_title = custom_title
254
283
  self.auto_size = auto_size
255
284
  self._padding = padding
285
+ self._vertical_offsets: dict[str, int] = self._BASE_VERTICAL_OFFSETS.copy()
256
286
 
257
287
  # Extract data from ChartDataModel
258
288
  self.chart_data = chart_data
@@ -265,12 +295,13 @@ class ChartDrawer:
265
295
  # SingleChartDataModel
266
296
  self.first_obj = getattr(chart_data, 'subject')
267
297
  self.second_obj = None
298
+
268
299
  else: # DualChartDataModel for Transit, Synastry, DualReturnChart
269
300
  self.first_obj = getattr(chart_data, 'first_subject')
270
301
  self.second_obj = getattr(chart_data, 'second_subject')
271
302
 
272
303
  # Load settings
273
- self.parse_json_settings(new_settings_file)
304
+ self._load_language_settings(language_pack)
274
305
 
275
306
  # Default radius for all charts
276
307
  self.main_radius = 240
@@ -282,12 +313,27 @@ class ChartDrawer:
282
313
  body["is_active"] = True
283
314
  self.available_planets_setting.append(body) # type: ignore[arg-type]
284
315
 
316
+ active_points_count = len(self.available_planets_setting)
317
+ if active_points_count > 24:
318
+ logging.warning(
319
+ "ChartDrawer detected %s active celestial points; rendering may look crowded beyond 24.",
320
+ active_points_count,
321
+ )
322
+
285
323
  # Set available celestial points
286
324
  available_celestial_points_names = [body["name"].lower() for body in self.available_planets_setting]
287
- self.available_kerykeion_celestial_points = []
288
- for body in available_celestial_points_names:
289
- if hasattr(self.first_obj, body):
290
- self.available_kerykeion_celestial_points.append(self.first_obj.get(body)) # type: ignore[arg-type]
325
+ self.available_kerykeion_celestial_points = self._collect_subject_points(
326
+ self.first_obj,
327
+ available_celestial_points_names,
328
+ )
329
+
330
+ # Collect secondary subject points for dual charts using the same active set
331
+ self.t_available_kerykeion_celestial_points: list[KerykeionPointModel] = []
332
+ if self.second_obj is not None:
333
+ self.t_available_kerykeion_celestial_points = self._collect_subject_points(
334
+ self.second_obj,
335
+ available_celestial_points_names,
336
+ )
291
337
 
292
338
  # ------------------------
293
339
  # CHART TYPE SPECIFIC SETUP FROM CHART DATA
@@ -340,9 +386,6 @@ class ChartDrawer:
340
386
  # Extract aspects from pre-computed chart data
341
387
  self.aspects_list = chart_data.aspects.relevant_aspects
342
388
 
343
- # Secondary subject available points
344
- self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
345
-
346
389
  # Screen size
347
390
  self.height = self._DEFAULT_HEIGHT
348
391
  if self.double_chart_aspect_grid_type == "table":
@@ -364,12 +407,9 @@ class ChartDrawer:
364
407
  # Extract aspects from pre-computed chart data
365
408
  self.aspects_list = chart_data.aspects.relevant_aspects
366
409
 
367
- # Secondary subject available points
368
- self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
369
-
370
410
  # Screen size
371
411
  self.height = self._DEFAULT_HEIGHT
372
- self.width = self._DEFAULT_FULL_WIDTH
412
+ self.width = self._DEFAULT_SYNASTRY_WIDTH
373
413
 
374
414
  # Get location and coordinates
375
415
  self.location, self.geolat, self.geolon = self._get_location_info()
@@ -385,9 +425,6 @@ class ChartDrawer:
385
425
  # Extract aspects from pre-computed chart data
386
426
  self.aspects_list = chart_data.aspects.relevant_aspects
387
427
 
388
- # Secondary subject available points
389
- self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
390
-
391
428
  # Screen size
392
429
  self.height = self._DEFAULT_HEIGHT
393
430
  self.width = self._DEFAULT_ULTRA_WIDE_WIDTH
@@ -438,20 +475,191 @@ class ChartDrawer:
438
475
 
439
476
  self.set_up_theme(theme)
440
477
 
441
- # Optionally expand width dynamically to fit content
478
+ self._apply_dynamic_height_adjustment()
479
+ self._adjust_height_for_extended_aspect_columns()
480
+ # Reconcile width with the updated layout once height adjustments are known.
442
481
  if self.auto_size:
443
- try:
444
- required_width = self._estimate_required_width_full()
445
- if required_width > self.width:
446
- self.width = required_width
447
- except Exception as e:
448
- # Keep default on any unexpected issue; do not break rendering
449
- logging.debug(f"Auto-size width calculation failed: {e}")
482
+ self._update_width_to_content()
450
483
 
451
484
  def _count_active_planets(self) -> int:
452
485
  """Return number of active celestial points in the current chart."""
453
486
  return len([p for p in self.available_planets_setting if p.get("is_active")])
454
487
 
488
+ def _apply_dynamic_height_adjustment(self) -> None:
489
+ """Adjust chart height and vertical offsets based on active points."""
490
+ active_points_count = self._count_active_planets()
491
+
492
+ offsets = self._BASE_VERTICAL_OFFSETS.copy()
493
+
494
+ minimum_height = self._DEFAULT_HEIGHT
495
+
496
+ if self.chart_type == "Synastry":
497
+ self._apply_synastry_height_adjustment(
498
+ active_points_count=active_points_count,
499
+ offsets=offsets,
500
+ minimum_height=minimum_height,
501
+ )
502
+ return
503
+
504
+ if active_points_count <= 20:
505
+ self.height = max(self.height, minimum_height)
506
+ self._vertical_offsets = offsets
507
+ return
508
+
509
+ extra_points = active_points_count - 20
510
+ extra_height = extra_points * self._ROW_HEIGHT
511
+
512
+ self.height = max(self.height, minimum_height + extra_height)
513
+
514
+ delta_height = max(self.height - minimum_height, 0)
515
+
516
+ # Anchor wheel, aspect grid/list, and lunar phase to the bottom
517
+ offsets["wheel"] += delta_height
518
+ offsets["aspect_grid"] += delta_height
519
+ offsets["aspect_list"] += delta_height
520
+ offsets["lunar_phase"] += delta_height
521
+ offsets["bottom_left"] += delta_height
522
+
523
+ # Smooth top offsets to keep breathing room near the title and grids
524
+ shift = min(extra_points * self._TOP_SHIFT_FACTOR, self._MAX_TOP_SHIFT)
525
+ top_shift = shift // 2
526
+
527
+ offsets["grid"] += shift
528
+ offsets["title"] += top_shift
529
+ offsets["elements"] += top_shift
530
+ offsets["qualities"] += top_shift
531
+
532
+ self._vertical_offsets = offsets
533
+
534
+ def _adjust_height_for_extended_aspect_columns(self) -> None:
535
+ """Ensure tall aspect columns fit within the SVG for double-chart lists."""
536
+ if self.double_chart_aspect_grid_type != "list":
537
+ return
538
+
539
+ if self.chart_type not in ("Synastry", "Transit", "DualReturnChart"):
540
+ return
541
+
542
+ total_aspects = len(self.aspects_list) if hasattr(self, "aspects_list") else 0
543
+ if total_aspects == 0:
544
+ return
545
+
546
+ aspects_per_column = 14
547
+ extended_column_start = 11 # Zero-based column index where tall columns begin
548
+ base_capacity = aspects_per_column * extended_column_start
549
+
550
+ if total_aspects <= base_capacity:
551
+ return
552
+
553
+ translate_y = 273
554
+ bottom_padding = 40
555
+ title_clearance = 18
556
+ line_height = 14
557
+ baseline_index = aspects_per_column - 1
558
+ top_limit_index = ceil((-translate_y + title_clearance) / line_height)
559
+ max_capacity_by_top = baseline_index - top_limit_index + 1
560
+
561
+ if max_capacity_by_top <= aspects_per_column:
562
+ return
563
+
564
+ target_capacity = max_capacity_by_top
565
+ required_available_height = target_capacity * line_height
566
+ required_height = translate_y + bottom_padding + required_available_height
567
+
568
+ if required_height <= self.height:
569
+ return
570
+
571
+ delta = required_height - self.height
572
+ self.height = required_height
573
+
574
+ offsets = self._vertical_offsets
575
+ # Keep bottom-anchored groups aligned after changing the overall height.
576
+ offsets["wheel"] += delta
577
+ offsets["aspect_grid"] += delta
578
+ offsets["aspect_list"] += delta
579
+ offsets["lunar_phase"] += delta
580
+ offsets["bottom_left"] += delta
581
+ self._vertical_offsets = offsets
582
+
583
+ def _apply_synastry_height_adjustment(
584
+ self,
585
+ *,
586
+ active_points_count: int,
587
+ offsets: dict[str, int],
588
+ minimum_height: int,
589
+ ) -> None:
590
+ """Specialised dynamic height handling for Synastry charts.
591
+
592
+ With the planet grids locked to a single column, every additional active
593
+ point extends multiple tables vertically (planets, houses, comparisons).
594
+ We therefore scale the height using the actual line spacing used by those
595
+ tables (≈14px) and keep the bottom anchored elements aligned.
596
+ """
597
+ base_rows = 14 # Up to 16 active points fit without extra height
598
+ extra_rows = max(active_points_count - base_rows, 0)
599
+
600
+ synastry_row_height = 15
601
+ comparison_padding_per_row = 4 # Keeps house comparison grids within view.
602
+ extra_height = extra_rows * (synastry_row_height + comparison_padding_per_row)
603
+
604
+ self.height = max(self.height, minimum_height + extra_height)
605
+
606
+ delta_height = max(self.height - minimum_height, 0)
607
+
608
+ offsets["wheel"] += delta_height
609
+ offsets["aspect_grid"] += delta_height
610
+ offsets["aspect_list"] += delta_height
611
+ offsets["lunar_phase"] += delta_height
612
+ offsets["bottom_left"] += delta_height
613
+
614
+ row_height_ratio = synastry_row_height / max(self._ROW_HEIGHT, 1)
615
+ synastry_top_shift_factor = max(
616
+ self._TOP_SHIFT_FACTOR,
617
+ int(ceil(self._TOP_SHIFT_FACTOR * row_height_ratio)),
618
+ )
619
+ shift = min(extra_rows * synastry_top_shift_factor, self._MAX_TOP_SHIFT)
620
+
621
+ base_grid_padding = 36
622
+ grid_padding_per_row = 6
623
+ base_header_padding = 12
624
+ header_padding_per_row = 4
625
+ min_title_to_grid_gap = 36
626
+
627
+ grid_shift = shift + base_grid_padding + (extra_rows * grid_padding_per_row)
628
+ grid_shift = min(grid_shift, shift + self._MAX_TOP_SHIFT)
629
+
630
+ top_shift = (shift // 2) + base_header_padding + (extra_rows * header_padding_per_row)
631
+
632
+ max_allowed_shift = shift + self._MAX_TOP_SHIFT
633
+ missing_gap = min_title_to_grid_gap - (grid_shift - top_shift)
634
+ grid_shift = min(grid_shift + missing_gap, max_allowed_shift)
635
+ if grid_shift - top_shift < min_title_to_grid_gap:
636
+ top_shift = max(0, grid_shift - min_title_to_grid_gap)
637
+
638
+ offsets["grid"] += grid_shift
639
+ offsets["title"] += top_shift
640
+ offsets["elements"] += top_shift
641
+ offsets["qualities"] += top_shift
642
+
643
+ self._vertical_offsets = offsets
644
+
645
+ def _collect_subject_points(
646
+ self,
647
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
648
+ point_attribute_names: list[str],
649
+ ) -> list[KerykeionPointModel]:
650
+ """Collect ordered active celestial points for a subject."""
651
+
652
+ collected: list[KerykeionPointModel] = []
653
+
654
+ for raw_name in point_attribute_names:
655
+ attr_name = raw_name if hasattr(subject, raw_name) else raw_name.lower()
656
+ point = getattr(subject, attr_name, None)
657
+ if point is None:
658
+ continue
659
+ collected.append(point)
660
+
661
+ return collected
662
+
455
663
  def _dynamic_viewbox(self) -> str:
456
664
  """Return the viewBox string based on current width/height."""
457
665
  return f"0 0 {int(self.width)} {int(self.height)}"
@@ -476,7 +684,7 @@ class ChartDrawer:
476
684
  uses `x_indent=50`, `y_indent=250`, `box_size=14` and draws:
477
685
  • a header row to the right of `x_indent`
478
686
  • a left header column at `x_indent - box_size`
479
- • an N×N grid of cells above `y_indent`
687
+ • an NxN grid of cells above `y_indent`
480
688
 
481
689
  - For Natal/Composite/SingleReturn charts, `draw_aspect_grid` uses
482
690
  `x_start=50`, `y_start=250`, `box_size=14` and draws a triangular grid
@@ -534,21 +742,17 @@ class ChartDrawer:
534
742
  extents.extend([main_planet_grid_right, main_houses_grid_right])
535
743
 
536
744
  if self.chart_type in ("Natal", "Composite", "SingleReturnChart"):
537
- # Triangular aspect grid at x_start=510, width ~ 14 * n_active
538
- aspect_grid_right = 510 + 14 * n_active
745
+ # Triangular aspect grid at x_start=540, width ~ 14 * n_active
746
+ aspect_grid_right = 560 + 14 * n_active
539
747
  extents.append(aspect_grid_right)
540
748
 
541
749
  if self.chart_type in ("Transit", "Synastry", "DualReturnChart"):
542
750
  # Double-chart aspects placement
543
751
  if self.double_chart_aspect_grid_type == "list":
544
- # Columnar list placed at translate(565,273), ~100-110px per column, 14 aspects per column
545
- aspects_per_column = 14
546
752
  total_aspects = len(self.aspects_list) if hasattr(self, "aspects_list") else 0
547
- columns = max((total_aspects + aspects_per_column - 1) // aspects_per_column, 1)
548
- # Respect the max columns cap used in rendering: DualReturn=7, others=6
549
- max_cols_cap = 7
550
- columns = min(columns, max_cols_cap)
551
- aspect_list_right = 565 + (columns * 110)
753
+ columns = self._calculate_double_chart_aspect_columns(total_aspects, self.height)
754
+ columns = max(columns, 1)
755
+ aspect_list_right = 565 + (columns * self._ASPECT_LIST_COLUMN_WIDTH)
552
756
  extents.append(aspect_list_right)
553
757
  else:
554
758
  # Grid table placed with x_indent ~550, width ~ 14px per cell across n_active+1
@@ -563,6 +767,9 @@ class ChartDrawer:
563
767
  # Secondary houses grid default x ~ 1015
564
768
  secondary_houses_grid_right = 1015 + 120
565
769
  extents.append(secondary_houses_grid_right)
770
+ first_house_comparison_grid_right = 1090 + 180
771
+ second_house_comparison_grid_right = 1290 + 180
772
+ extents.extend([first_house_comparison_grid_right, second_house_comparison_grid_right])
566
773
 
567
774
  if self.chart_type == "Transit":
568
775
  # House comparison grid at x ~ 1030
@@ -577,6 +784,84 @@ class ChartDrawer:
577
784
  # Conservative safety padding
578
785
  return int(max(extents) + self._padding)
579
786
 
787
+ def _calculate_double_chart_aspect_columns(
788
+ self,
789
+ total_aspects: int,
790
+ chart_height: Optional[int],
791
+ ) -> int:
792
+ """Return how many columns the double-chart aspect list needs.
793
+
794
+ The first 11 columns follow the legacy 14-rows layout. Starting from the
795
+ 12th column we can fit more rows thanks to the taller chart height that
796
+ gets computed earlier, so we re-use the same capacity as the SVG builder.
797
+ """
798
+ if total_aspects <= 0:
799
+ return 0
800
+
801
+ per_column = self._ASPECT_LIST_ASPECTS_PER_COLUMN
802
+ extended_start = 10 # 0-based index where tall columns begin
803
+ base_capacity = per_column * extended_start
804
+
805
+ full_height_capacity = self._calculate_full_height_column_capacity(chart_height)
806
+
807
+ if total_aspects <= base_capacity:
808
+ return ceil(total_aspects / per_column)
809
+
810
+ remaining = max(total_aspects - base_capacity, 0)
811
+ extra_columns = ceil(remaining / full_height_capacity) if remaining > 0 else 0
812
+ return extended_start + extra_columns
813
+
814
+ def _calculate_full_height_column_capacity(
815
+ self,
816
+ chart_height: Optional[int],
817
+ ) -> int:
818
+ """Compute the row capacity for columns that use the tall layout."""
819
+ per_column = self._ASPECT_LIST_ASPECTS_PER_COLUMN
820
+
821
+ if chart_height is None:
822
+ return per_column
823
+
824
+ translate_y = 273
825
+ bottom_padding = 40
826
+ title_clearance = 18
827
+ line_height = 14
828
+ baseline_index = per_column - 1
829
+ top_limit_index = ceil((-translate_y + title_clearance) / line_height)
830
+ max_capacity_by_top = baseline_index - top_limit_index + 1
831
+
832
+ available_height = max(chart_height - translate_y - bottom_padding, line_height)
833
+ allowed_capacity = max(per_column, int(available_height // line_height))
834
+
835
+ # Respect both the physical height of the SVG and the visual limit
836
+ # imposed by the title area.
837
+ return max(per_column, min(allowed_capacity, max_capacity_by_top))
838
+
839
+ def _minimum_width_for_chart_type(self) -> int:
840
+ """Baseline width to avoid compressing core groups too tightly."""
841
+ wheel_right = 100 + (2 * self.main_radius)
842
+ baseline = wheel_right + self._padding
843
+
844
+ if self.chart_type in ("Natal", "Composite", "SingleReturnChart"):
845
+ return max(int(baseline), self._DEFAULT_NATAL_WIDTH)
846
+ if self.chart_type == "Synastry":
847
+ return max(int(baseline), self._DEFAULT_SYNASTRY_WIDTH // 2)
848
+ if self.chart_type == "DualReturnChart":
849
+ return max(int(baseline), self._DEFAULT_ULTRA_WIDE_WIDTH // 2)
850
+ if self.chart_type == "Transit":
851
+ return max(int(baseline), self._DEFAULT_FULL_WIDTH // 2)
852
+ return max(int(baseline), self._DEFAULT_NATAL_WIDTH)
853
+
854
+ def _update_width_to_content(self) -> None:
855
+ """Resize the chart width so the farthest element fits comfortably."""
856
+ try:
857
+ required_width = self._estimate_required_width_full()
858
+ except Exception as e:
859
+ logging.debug(f"Auto-size width calculation failed: {e}")
860
+ return
861
+
862
+ minimum_width = self._minimum_width_for_chart_type()
863
+ self.width = max(required_width, minimum_width)
864
+
580
865
  def _get_location_info(self) -> tuple[str, float, float]:
581
866
  """
582
867
  Determine location information based on chart type and subjects.
@@ -624,17 +909,31 @@ class ChartDrawer:
624
909
  with open(theme_dir / f"{theme}.css", "r") as f:
625
910
  self.color_style_tag = f.read()
626
911
 
627
- def parse_json_settings(self, settings_file_or_dict: Union[Path, dict, KerykeionSettingsModel, None]) -> None:
628
- """
629
- Load and parse chart configuration settings.
630
-
631
- Args:
632
- settings_file_or_dict (Path, dict, or KerykeionSettingsModel):
633
- Source for custom chart settings.
634
- """
635
- settings = get_settings(settings_file_or_dict)
636
-
637
- self.language_settings = settings["language_settings"][self.chart_language]
912
+ def _load_language_settings(
913
+ self,
914
+ language_pack: Optional[Mapping[str, Any]],
915
+ ) -> None:
916
+ """Resolve language models for the requested chart language."""
917
+ overrides = {self.chart_language: dict(language_pack)} if language_pack else None
918
+ languages = load_language_settings(overrides)
919
+
920
+ fallback_data = languages.get("EN")
921
+ if fallback_data is None:
922
+ raise KerykeionException("English translations are missing from LANGUAGE_SETTINGS.")
923
+
924
+ base_data = languages.get(self.chart_language, fallback_data)
925
+ selected_model = KerykeionLanguageModel(**base_data)
926
+ fallback_model = KerykeionLanguageModel(**fallback_data)
927
+
928
+ self._fallback_language_model = fallback_model
929
+ self._language_model = selected_model
930
+ self._fallback_language_dict = fallback_model.model_dump()
931
+ self._language_dict = selected_model.model_dump()
932
+ self.language_settings = self._language_dict # Backward compatibility
933
+
934
+ def _translate(self, key: str, default: Any) -> Any:
935
+ fallback_value = get_translations(key, default, language_dict=self._fallback_language_dict)
936
+ return get_translations(key, fallback_value, language_dict=self._language_dict)
638
937
 
639
938
  def _draw_zodiac_circle_slices(self, r):
640
939
  """
@@ -711,7 +1010,7 @@ class ChartDrawer:
711
1010
  )
712
1011
  return out
713
1012
 
714
- def _truncate_name(self, name: str, max_length: int = 50) -> str:
1013
+ def _truncate_name(self, name: str, max_length: int = 50, ellipsis_symbol: str = "…", truncate_at_space: bool = False) -> str:
715
1014
  """
716
1015
  Truncate a name if it's too long, preserving readability.
717
1016
 
@@ -722,9 +1021,13 @@ class ChartDrawer:
722
1021
  Returns:
723
1022
  str: Truncated name with ellipsis if needed
724
1023
  """
1024
+ if truncate_at_space:
1025
+ name = name.split(" ")[0]
1026
+
725
1027
  if len(name) <= max_length:
726
1028
  return name
727
- return name[:max_length-1] + "…"
1029
+
1030
+ return name[:max_length-1] + ellipsis_symbol
728
1031
 
729
1032
  def _get_chart_title(self) -> str:
730
1033
  """
@@ -742,52 +1045,49 @@ class ChartDrawer:
742
1045
 
743
1046
  # Generate default title based on chart type
744
1047
  if self.chart_type == "Natal":
745
- natal_label = self.language_settings.get("birth_chart", "Natal")
1048
+ natal_label = self._translate("birth_chart", "Natal")
746
1049
  truncated_name = self._truncate_name(self.first_obj.name)
747
1050
  return f'{truncated_name} - {natal_label}'
748
1051
 
749
1052
  elif self.chart_type == "Composite":
750
- composite_label = self.language_settings.get("composite_chart", "Composite")
751
- and_word = self.language_settings.get("and_word", "&")
1053
+ composite_label = self._translate("composite_chart", "Composite")
1054
+ and_word = self._translate("and_word", "&")
752
1055
  name1 = self._truncate_name(self.first_obj.first_subject.name) # type: ignore
753
1056
  name2 = self._truncate_name(self.first_obj.second_subject.name) # type: ignore
754
1057
  return f"{composite_label}: {name1} {and_word} {name2}"
755
1058
 
756
1059
  elif self.chart_type == "Transit":
757
- transit_label = self.language_settings.get("transits", "Transits")
758
- from datetime import datetime
1060
+ transit_label = self._translate("transits", "Transits")
759
1061
  date_obj = datetime.fromisoformat(self.second_obj.iso_formatted_local_datetime) # type: ignore
760
1062
  date_str = date_obj.strftime("%d/%m/%y")
761
1063
  truncated_name = self._truncate_name(self.first_obj.name)
762
1064
  return f"{truncated_name} - {transit_label} {date_str}"
763
1065
 
764
1066
  elif self.chart_type == "Synastry":
765
- synastry_label = self.language_settings.get("synastry_chart", "Synastry")
766
- and_word = self.language_settings.get("and_word", "&")
1067
+ synastry_label = self._translate("synastry_chart", "Synastry")
1068
+ and_word = self._translate("and_word", "&")
767
1069
  name1 = self._truncate_name(self.first_obj.name)
768
1070
  name2 = self._truncate_name(self.second_obj.name) # type: ignore
769
1071
  return f"{synastry_label}: {name1} {and_word} {name2}"
770
1072
 
771
1073
  elif self.chart_type == "DualReturnChart":
772
- from datetime import datetime
773
1074
  year = datetime.fromisoformat(self.second_obj.iso_formatted_local_datetime).year # type: ignore
774
1075
  truncated_name = self._truncate_name(self.first_obj.name)
775
1076
  if self.second_obj is not None and isinstance(self.second_obj, PlanetReturnModel) and self.second_obj.return_type == "Solar":
776
- solar_label = self.language_settings.get("solar_return", "Solar")
1077
+ solar_label = self._translate("solar_return", "Solar")
777
1078
  return f"{truncated_name} - {solar_label} {year}"
778
1079
  else:
779
- lunar_label = self.language_settings.get("lunar_return", "Lunar")
1080
+ lunar_label = self._translate("lunar_return", "Lunar")
780
1081
  return f"{truncated_name} - {lunar_label} {year}"
781
1082
 
782
1083
  elif self.chart_type == "SingleReturnChart":
783
- from datetime import datetime
784
1084
  year = datetime.fromisoformat(self.first_obj.iso_formatted_local_datetime).year # type: ignore
785
1085
  truncated_name = self._truncate_name(self.first_obj.name)
786
1086
  if isinstance(self.first_obj, PlanetReturnModel) and self.first_obj.return_type == "Solar":
787
- solar_label = self.language_settings.get("solar_return", "Solar")
1087
+ solar_label = self._translate("solar_return", "Solar")
788
1088
  return f"{truncated_name} - {solar_label} {year}"
789
1089
  else:
790
- lunar_label = self.language_settings.get("lunar_return", "Lunar")
1090
+ lunar_label = self._translate("lunar_return", "Lunar")
791
1091
  return f"{truncated_name} - {lunar_label} {year}"
792
1092
 
793
1093
  # Fallback for unknown chart types
@@ -815,6 +1115,17 @@ class ChartDrawer:
815
1115
  template_dict["chart_height"] = self.height
816
1116
  template_dict["chart_width"] = self.width
817
1117
 
1118
+ offsets = self._vertical_offsets
1119
+ template_dict["full_wheel_translate_y"] = offsets["wheel"]
1120
+ template_dict["houses_and_planets_translate_y"] = offsets["grid"]
1121
+ template_dict["aspect_grid_translate_y"] = offsets["aspect_grid"]
1122
+ template_dict["aspect_list_translate_y"] = offsets["aspect_list"]
1123
+ template_dict["title_translate_y"] = offsets["title"]
1124
+ template_dict["elements_translate_y"] = offsets["elements"]
1125
+ template_dict["qualities_translate_y"] = offsets["qualities"]
1126
+ template_dict["lunar_phase_translate_y"] = offsets["lunar_phase"]
1127
+ template_dict["bottom_left_translate_y"] = offsets["bottom_left"]
1128
+
818
1129
  # Set paper colors
819
1130
  template_dict["paper_color_0"] = self.chart_colors_settings["paper_0"]
820
1131
 
@@ -855,11 +1166,11 @@ class ChartDrawer:
855
1166
  water_percentage = element_percentages["water"]
856
1167
 
857
1168
  # Element Percentages
858
- template_dict["elements_string"] = f"{self.language_settings.get('elements', 'Elements')}:"
859
- template_dict["fire_string"] = f"{self.language_settings['fire']} {fire_percentage}%"
860
- template_dict["earth_string"] = f"{self.language_settings['earth']} {earth_percentage}%"
861
- template_dict["air_string"] = f"{self.language_settings['air']} {air_percentage}%"
862
- template_dict["water_string"] = f"{self.language_settings['water']} {water_percentage}%"
1169
+ template_dict["elements_string"] = f"{self._translate('elements', 'Elements')}:"
1170
+ template_dict["fire_string"] = f"{self._translate('fire', 'Fire')} {fire_percentage}%"
1171
+ template_dict["earth_string"] = f"{self._translate('earth', 'Earth')} {earth_percentage}%"
1172
+ template_dict["air_string"] = f"{self._translate('air', 'Air')} {air_percentage}%"
1173
+ template_dict["water_string"] = f"{self._translate('water', 'Water')} {water_percentage}%"
863
1174
 
864
1175
 
865
1176
  # Qualities Percentages
@@ -870,10 +1181,10 @@ class ChartDrawer:
870
1181
  fixed_percentage = quality_percentages["fixed"]
871
1182
  mutable_percentage = quality_percentages["mutable"]
872
1183
 
873
- template_dict["qualities_string"] = f"{self.language_settings.get('qualities', 'Qualities')}:"
874
- template_dict["cardinal_string"] = f"{self.language_settings.get('cardinal', 'Cardinal')} {cardinal_percentage}%"
875
- template_dict["fixed_string"] = f"{self.language_settings.get('fixed', 'Fixed')} {fixed_percentage}%"
876
- template_dict["mutable_string"] = f"{self.language_settings.get('mutable', 'Mutable')} {mutable_percentage}%"
1184
+ template_dict["qualities_string"] = f"{self._translate('qualities', 'Qualities')}:"
1185
+ template_dict["cardinal_string"] = f"{self._translate('cardinal', 'Cardinal')} {cardinal_percentage}%"
1186
+ template_dict["fixed_string"] = f"{self._translate('fixed', 'Fixed')} {fixed_percentage}%"
1187
+ template_dict["mutable_string"] = f"{self._translate('mutable', 'Mutable')} {mutable_percentage}%"
877
1188
 
878
1189
  # Get houses list for main subject
879
1190
  first_subject_houses_list = get_houses_list(self.first_obj)
@@ -933,37 +1244,60 @@ class ChartDrawer:
933
1244
  template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
934
1245
 
935
1246
  # Top left section
936
- latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
937
- longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings["east"], self.language_settings["west"])
1247
+ latitude_string = convert_latitude_coordinate_to_string(
1248
+ self.geolat,
1249
+ self._translate("north", "North"),
1250
+ self._translate("south", "South"),
1251
+ )
1252
+ longitude_string = convert_longitude_coordinate_to_string(
1253
+ self.geolon,
1254
+ self._translate("east", "East"),
1255
+ self._translate("west", "West"),
1256
+ )
938
1257
 
939
- template_dict["top_left_0"] = f'{self.language_settings.get("location", "Location")}:'
1258
+ template_dict["top_left_0"] = f'{self._translate("location", "Location")}:'
940
1259
  template_dict["top_left_1"] = f"{self.first_obj.city}, {self.first_obj.nation}"
941
- template_dict["top_left_2"] = f"{self.language_settings['latitude']}: {latitude_string}"
942
- template_dict["top_left_3"] = f"{self.language_settings['longitude']}: {longitude_string}"
1260
+ template_dict["top_left_2"] = f"{self._translate('latitude', 'Latitude')}: {latitude_string}"
1261
+ template_dict["top_left_3"] = f"{self._translate('longitude', 'Longitude')}: {longitude_string}"
943
1262
  template_dict["top_left_4"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
944
- localized_weekday = self.language_settings.get('weekdays', {}).get(self.first_obj.day_of_week, self.first_obj.day_of_week) # type: ignore
945
- template_dict["top_left_5"] = f"{self.language_settings.get('day_of_week', 'Day of Week')}: {localized_weekday}" # type: ignore
1263
+ localized_weekday = self._translate(
1264
+ f"weekdays.{self.first_obj.day_of_week}",
1265
+ self.first_obj.day_of_week, # type: ignore[arg-type]
1266
+ )
1267
+ template_dict["top_left_5"] = f"{self._translate('day_of_week', 'Day of Week')}: {localized_weekday}" # type: ignore
946
1268
 
947
1269
  # Bottom left section
948
1270
  if self.first_obj.zodiac_type == "Tropic":
949
- zodiac_info = f"{self.language_settings.get('zodiac', 'Zodiac')}: {self.language_settings.get('tropical', 'Tropical')}"
1271
+ zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
950
1272
  else:
951
1273
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
952
1274
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
953
- zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
1275
+ zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
954
1276
 
955
1277
  template_dict["bottom_left_0"] = zodiac_info
956
- template_dict["bottom_left_1"] = f"{self.language_settings.get('domification', 'Domification')}: {self.language_settings.get('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)}"
1278
+ template_dict["bottom_left_1"] = (
1279
+ f"{self._translate('domification', 'Domification')}: "
1280
+ f"{self._translate('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)}"
1281
+ )
957
1282
 
958
1283
  # Lunar phase information (optional)
959
1284
  if self.first_obj.lunar_phase is not None:
960
- template_dict["bottom_left_2"] = f'{self.language_settings.get("lunation_day", "Lunation Day")}: {self.first_obj.lunar_phase.get("moon_phase", "")}'
961
- template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get(self.first_obj.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.first_obj.lunar_phase.moon_phase_name)}'
1285
+ template_dict["bottom_left_2"] = (
1286
+ f'{self._translate("lunation_day", "Lunation Day")}: '
1287
+ f'{self.first_obj.lunar_phase.get("moon_phase", "")}'
1288
+ )
1289
+ template_dict["bottom_left_3"] = (
1290
+ f'{self._translate("lunar_phase", "Lunar Phase")}: '
1291
+ f'{self._translate(self.first_obj.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.first_obj.lunar_phase.moon_phase_name)}'
1292
+ )
962
1293
  else:
963
1294
  template_dict["bottom_left_2"] = ""
964
1295
  template_dict["bottom_left_3"] = ""
965
1296
 
966
- template_dict["bottom_left_4"] = f'{self.language_settings.get("perspective_type", "Perspective")}: {self.language_settings.get(self.first_obj.perspective_type.lower().replace(" ", "_"), self.first_obj.perspective_type)}'
1297
+ template_dict["bottom_left_4"] = (
1298
+ f'{self._translate("perspective_type", "Perspective")}: '
1299
+ f'{self._translate(self.first_obj.perspective_type.lower().replace(" ", "_"), self.first_obj.perspective_type)}'
1300
+ )
967
1301
 
968
1302
  # Moon phase section calculations
969
1303
  if self.first_obj.lunar_phase is not None:
@@ -975,7 +1309,7 @@ class ChartDrawer:
975
1309
  template_dict["makeMainHousesGrid"] = draw_main_house_grid(
976
1310
  main_subject_houses_list=first_subject_houses_list,
977
1311
  text_color=self.chart_colors_settings["paper_0"],
978
- house_cusp_generale_name_label=self.language_settings["cusp"],
1312
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
979
1313
  )
980
1314
  template_dict["makeSecondaryHousesGrid"] = ""
981
1315
 
@@ -1005,12 +1339,12 @@ class ChartDrawer:
1005
1339
  )
1006
1340
 
1007
1341
  template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
1008
- planets_and_houses_grid_title=self.language_settings["planets_and_house"],
1342
+ planets_and_houses_grid_title=self._translate("planets_and_house", "Points for"),
1009
1343
  subject_name=self.first_obj.name,
1010
1344
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
1011
1345
  chart_type=self.chart_type,
1012
1346
  text_color=self.chart_colors_settings["paper_0"],
1013
- celestial_point_language=self.language_settings["celestial_points"],
1347
+ celestial_point_language=self._language_model.celestial_points,
1014
1348
  )
1015
1349
  template_dict["makeSecondaryPlanetGrid"] = ""
1016
1350
  template_dict["makeHouseComparisonGrid"] = ""
@@ -1066,25 +1400,25 @@ class ChartDrawer:
1066
1400
  # First subject
1067
1401
  latitude = convert_latitude_coordinate_to_string(
1068
1402
  self.first_obj.first_subject.lat, # type: ignore
1069
- self.language_settings["north_letter"],
1070
- self.language_settings["south_letter"],
1403
+ self._translate("north_letter", "N"),
1404
+ self._translate("south_letter", "S"),
1071
1405
  )
1072
1406
  longitude = convert_longitude_coordinate_to_string(
1073
1407
  self.first_obj.first_subject.lng, # type: ignore
1074
- self.language_settings["east_letter"],
1075
- self.language_settings["west_letter"],
1408
+ self._translate("east_letter", "E"),
1409
+ self._translate("west_letter", "W"),
1076
1410
  )
1077
1411
 
1078
1412
  # Second subject
1079
1413
  latitude_string = convert_latitude_coordinate_to_string(
1080
1414
  self.first_obj.second_subject.lat, # type: ignore
1081
- self.language_settings["north_letter"],
1082
- self.language_settings["south_letter"],
1415
+ self._translate("north_letter", "N"),
1416
+ self._translate("south_letter", "S"),
1083
1417
  )
1084
1418
  longitude_string = convert_longitude_coordinate_to_string(
1085
1419
  self.first_obj.second_subject.lng, # type: ignore
1086
- self.language_settings["east_letter"],
1087
- self.language_settings["west_letter"],
1420
+ self._translate("east_letter", "E"),
1421
+ self._translate("west_letter", "W"),
1088
1422
  )
1089
1423
 
1090
1424
  template_dict["top_left_0"] = f"{self.first_obj.first_subject.name}" # type: ignore
@@ -1096,16 +1430,16 @@ class ChartDrawer:
1096
1430
 
1097
1431
  # Bottom left section
1098
1432
  if self.first_obj.zodiac_type == "Tropic":
1099
- zodiac_info = f"{self.language_settings.get('zodiac', 'Zodiac')}: {self.language_settings.get('tropical', 'Tropical')}"
1433
+ zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1100
1434
  else:
1101
1435
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
1102
1436
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
1103
- zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
1437
+ zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
1104
1438
 
1105
1439
  template_dict["bottom_left_0"] = zodiac_info
1106
- template_dict["bottom_left_1"] = f"{self.language_settings.get('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)} {self.language_settings.get('houses', 'Houses')}"
1107
- template_dict["bottom_left_2"] = f'{self.language_settings.get("perspective_type", "Perspective")}: {self.first_obj.first_subject.perspective_type}' # type: ignore
1108
- template_dict["bottom_left_3"] = f'{self.language_settings.get("composite_chart", "Composite Chart")} - {self.language_settings.get("midpoints", "Midpoints")}'
1440
+ template_dict["bottom_left_1"] = f"{self._translate('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)} {self._translate('houses', 'Houses')}"
1441
+ template_dict["bottom_left_2"] = f'{self._translate("perspective_type", "Perspective")}: {self.first_obj.first_subject.perspective_type}' # type: ignore
1442
+ template_dict["bottom_left_3"] = f'{self._translate("composite_chart", "Composite Chart")} - {self._translate("midpoints", "Midpoints")}'
1109
1443
  template_dict["bottom_left_4"] = ""
1110
1444
 
1111
1445
  # Moon phase section calculations
@@ -1118,7 +1452,7 @@ class ChartDrawer:
1118
1452
  template_dict["makeMainHousesGrid"] = draw_main_house_grid(
1119
1453
  main_subject_houses_list=first_subject_houses_list,
1120
1454
  text_color=self.chart_colors_settings["paper_0"],
1121
- house_cusp_generale_name_label=self.language_settings["cusp"],
1455
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1122
1456
  )
1123
1457
  template_dict["makeSecondaryHousesGrid"] = ""
1124
1458
 
@@ -1147,15 +1481,19 @@ class ChartDrawer:
1147
1481
  external_view=self.external_view,
1148
1482
  )
1149
1483
 
1150
- subject_name = f"{self.first_obj.first_subject.name} {self.language_settings['and_word']} {self.first_obj.second_subject.name}" # type: ignore
1484
+ subject_name = (
1485
+ f"{self.first_obj.first_subject.name}"
1486
+ f" {self._translate('and_word', '&')} "
1487
+ f"{self.first_obj.second_subject.name}"
1488
+ ) # type: ignore
1151
1489
 
1152
1490
  template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
1153
- planets_and_houses_grid_title=self.language_settings["planets_and_house"],
1491
+ planets_and_houses_grid_title=self._translate("planets_and_house", "Points for"),
1154
1492
  subject_name=subject_name,
1155
1493
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
1156
1494
  chart_type=self.chart_type,
1157
1495
  text_color=self.chart_colors_settings["paper_0"],
1158
- celestial_point_language=self.language_settings["celestial_points"],
1496
+ celestial_point_language=self._language_model.celestial_points,
1159
1497
  )
1160
1498
  template_dict["makeSecondaryPlanetGrid"] = ""
1161
1499
  template_dict["makeHouseComparisonGrid"] = ""
@@ -1214,52 +1552,58 @@ class ChartDrawer:
1214
1552
 
1215
1553
  # Aspects
1216
1554
  if self.double_chart_aspect_grid_type == "list":
1217
- title = f'{self.first_obj.name} - {self.language_settings.get("transit_aspects", "Transit Aspects")}'
1555
+ title = f'{self.first_obj.name} - {self._translate("transit_aspects", "Transit Aspects")}'
1218
1556
  template_dict["makeAspectGrid"] = ""
1219
- template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings) # type: ignore[arg-type] # type: ignore[arg-type]
1557
+ template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(
1558
+ title,
1559
+ self.aspects_list,
1560
+ self.planets_settings,
1561
+ self.aspects_settings,
1562
+ chart_height=self.height,
1563
+ ) # type: ignore[arg-type]
1220
1564
  else:
1221
1565
  template_dict["makeAspectGrid"] = ""
1222
1566
  template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
1223
1567
  self.chart_colors_settings["paper_0"],
1224
1568
  self.available_planets_setting,
1225
1569
  self.aspects_list,
1226
- 550,
1227
- 450,
1570
+ 600,
1571
+ 520,
1228
1572
  )
1229
1573
 
1230
1574
  template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
1231
1575
 
1232
1576
  # Top left section
1233
- latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
1234
- longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings["east"], self.language_settings["west"])
1577
+ latitude_string = convert_latitude_coordinate_to_string(self.geolat, self._translate("north", "North"), self._translate("south", "South"))
1578
+ longitude_string = convert_longitude_coordinate_to_string(self.geolon, self._translate("east", "East"), self._translate("west", "West"))
1235
1579
 
1236
- template_dict["top_left_0"] = template_dict["top_left_0"] = f'{self.first_obj.name}'
1580
+ template_dict["top_left_0"] = f"{self.first_obj.name}"
1237
1581
  template_dict["top_left_1"] = f"{format_location_string(self.first_obj.city)}, {self.first_obj.nation}" # type: ignore
1238
1582
  template_dict["top_left_2"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
1239
- template_dict["top_left_3"] = f"{self.language_settings['latitude']}: {latitude_string}"
1240
- template_dict["top_left_4"] = f"{self.language_settings['longitude']}: {longitude_string}"
1241
- template_dict["top_left_5"] = ""#f"{self.language_settings['type']}: {self.language_settings.get(self.chart_type, self.chart_type)}"
1583
+ template_dict["top_left_3"] = f"{self._translate('latitude', 'Latitude')}: {latitude_string}"
1584
+ template_dict["top_left_4"] = f"{self._translate('longitude', 'Longitude')}: {longitude_string}"
1585
+ template_dict["top_left_5"] = ""#f"{self._translate('type', 'Type')}: {self._translate(self.chart_type, self.chart_type)}"
1242
1586
 
1243
1587
  # Bottom left section
1244
1588
  if self.first_obj.zodiac_type == "Tropic":
1245
- zodiac_info = f"{self.language_settings.get('zodiac', 'Zodiac')}: {self.language_settings.get('tropical', 'Tropical')}"
1589
+ zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1246
1590
  else:
1247
1591
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
1248
1592
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
1249
- zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
1593
+ zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
1250
1594
 
1251
1595
  template_dict["bottom_left_0"] = zodiac_info
1252
- template_dict["bottom_left_1"] = f"{self.language_settings.get('domification', 'Domification')}: {self.language_settings.get('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)}"
1596
+ template_dict["bottom_left_1"] = f"{self._translate('domification', 'Domification')}: {self._translate('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)}"
1253
1597
 
1254
1598
  # Lunar phase information from second object (Transit) (optional)
1255
1599
  if self.second_obj is not None and hasattr(self.second_obj, 'lunar_phase') and self.second_obj.lunar_phase is not None:
1256
- template_dict["bottom_left_2"] = f'{self.language_settings.get("lunation_day", "Lunation Day")}: {self.second_obj.lunar_phase.get("moon_phase", "")}' # type: ignore
1257
- template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get(self.second_obj.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.second_obj.lunar_phase.moon_phase_name)}'
1600
+ template_dict["bottom_left_2"] = f'{self._translate("lunation_day", "Lunation Day")}: {self.second_obj.lunar_phase.get("moon_phase", "")}' # type: ignore
1601
+ template_dict["bottom_left_3"] = f'{self._translate("lunar_phase", "Lunar Phase")}: {self._translate(self.second_obj.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.second_obj.lunar_phase.moon_phase_name)}'
1258
1602
  else:
1259
1603
  template_dict["bottom_left_2"] = ""
1260
1604
  template_dict["bottom_left_3"] = ""
1261
1605
 
1262
- template_dict["bottom_left_4"] = f'{self.language_settings.get("perspective_type", "Perspective")}: {self.language_settings.get(self.second_obj.perspective_type.lower().replace(" ", "_"), self.second_obj.perspective_type)}' # type: ignore
1606
+ template_dict["bottom_left_4"] = f'{self._translate("perspective_type", "Perspective")}: {self._translate(self.second_obj.perspective_type.lower().replace(" ", "_"), self.second_obj.perspective_type)}' # type: ignore
1263
1607
 
1264
1608
  # Moon phase section calculations - use first_obj for visualization
1265
1609
  if self.first_obj.lunar_phase is not None:
@@ -1271,12 +1615,12 @@ class ChartDrawer:
1271
1615
  template_dict["makeMainHousesGrid"] = draw_main_house_grid(
1272
1616
  main_subject_houses_list=first_subject_houses_list,
1273
1617
  text_color=self.chart_colors_settings["paper_0"],
1274
- house_cusp_generale_name_label=self.language_settings["cusp"],
1618
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1275
1619
  )
1276
1620
  # template_dict["makeSecondaryHousesGrid"] = draw_secondary_house_grid(
1277
1621
  # secondary_subject_houses_list=second_subject_houses_list,
1278
1622
  # text_color=self.chart_colors_settings["paper_0"],
1279
- # house_cusp_generale_name_label=self.language_settings["cusp"],
1623
+ # house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1280
1624
  # )
1281
1625
  template_dict["makeSecondaryHousesGrid"] = ""
1282
1626
 
@@ -1309,15 +1653,17 @@ class ChartDrawer:
1309
1653
  )
1310
1654
 
1311
1655
  # Planet grids
1312
- first_return_grid_title = f"{self.first_obj.name} ({self.language_settings.get('inner_wheel', 'Inner Wheel')})"
1313
- second_return_grid_title = f"{self.language_settings.get('Transit', 'Transit')} ({self.language_settings.get('outer_wheel', 'Outer Wheel')})"
1656
+ first_name_label = self._truncate_name(self.first_obj.name)
1657
+ transit_label = self._translate("transit", "Transit")
1658
+ first_return_grid_title = f"{first_name_label} ({self._translate('inner_wheel', 'Inner Wheel')})"
1659
+ second_return_grid_title = f"{transit_label} ({self._translate('outer_wheel', 'Outer Wheel')})"
1314
1660
  template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
1315
1661
  planets_and_houses_grid_title="",
1316
1662
  subject_name=first_return_grid_title,
1317
1663
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
1318
1664
  chart_type=self.chart_type,
1319
1665
  text_color=self.chart_colors_settings["paper_0"],
1320
- celestial_point_language=self.language_settings["celestial_points"],
1666
+ celestial_point_language=self._language_model.celestial_points,
1321
1667
  )
1322
1668
 
1323
1669
  template_dict["makeSecondaryPlanetGrid"] = draw_secondary_planet_grid(
@@ -1326,7 +1672,7 @@ class ChartDrawer:
1326
1672
  second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
1327
1673
  chart_type=self.chart_type,
1328
1674
  text_color=self.chart_colors_settings["paper_0"],
1329
- celestial_point_language=self.language_settings["celestial_points"],
1675
+ celestial_point_language=self._language_model.celestial_points,
1330
1676
  )
1331
1677
 
1332
1678
  # House comparison grid
@@ -1339,12 +1685,12 @@ class ChartDrawer:
1339
1685
 
1340
1686
  template_dict["makeHouseComparisonGrid"] = draw_single_house_comparison_grid(
1341
1687
  house_comparison,
1342
- celestial_point_language=self.language_settings.get("celestial_points", "Celestial Points"),
1688
+ celestial_point_language=self._language_model.celestial_points,
1343
1689
  active_points=self.active_points,
1344
1690
  points_owner_subject_number=2, # The second subject is the Transit
1345
- house_position_comparison_label=self.language_settings.get("house_position_comparison", "House Position Comparison"),
1346
- return_point_label=self.language_settings.get("transit_point", "Transit Point"),
1347
- natal_house_label=self.language_settings.get("house_position", "Natal House"),
1691
+ house_position_comparison_label=self._translate("house_position_comparison", "House Position Comparison"),
1692
+ return_point_label=self._translate("transit_point", "Transit Point"),
1693
+ natal_house_label=self._translate("house_position", "Natal House"),
1348
1694
  x_position=980,
1349
1695
  )
1350
1696
 
@@ -1390,10 +1736,11 @@ class ChartDrawer:
1390
1736
  if self.double_chart_aspect_grid_type == "list":
1391
1737
  template_dict["makeAspectGrid"] = ""
1392
1738
  template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(
1393
- f"{self.first_obj.name} - {self.second_obj.name} {self.language_settings.get('synastry_aspects', 'Synastry Aspects')}", # type: ignore[union-attr]
1739
+ f"{self.first_obj.name} - {self.second_obj.name} {self._translate('synastry_aspects', 'Synastry Aspects')}", # type: ignore[union-attr]
1394
1740
  self.aspects_list,
1395
1741
  self.planets_settings, # type: ignore[arg-type]
1396
- self.aspects_settings # type: ignore[arg-type]
1742
+ self.aspects_settings, # type: ignore[arg-type]
1743
+ chart_height=self.height,
1397
1744
  )
1398
1745
  else:
1399
1746
  template_dict["makeAspectGrid"] = ""
@@ -1417,18 +1764,18 @@ class ChartDrawer:
1417
1764
 
1418
1765
  # Bottom left section
1419
1766
  if self.first_obj.zodiac_type == "Tropic":
1420
- zodiac_info = f"{self.language_settings.get('zodiac', 'Zodiac')}: {self.language_settings.get('tropical', 'Tropical')}"
1767
+ zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1421
1768
  else:
1422
1769
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
1423
1770
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
1424
- zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
1771
+ zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
1425
1772
 
1426
1773
  template_dict["bottom_left_0"] = ""
1427
1774
  # FIXME!
1428
1775
  template_dict["bottom_left_1"] = "" # f"Compatibility Score: {16}/44" # type: ignore
1429
1776
  template_dict["bottom_left_2"] = zodiac_info
1430
- template_dict["bottom_left_3"] = f"{self.language_settings.get('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)} {self.language_settings.get('houses', 'Houses')}"
1431
- template_dict["bottom_left_4"] = f'{self.language_settings.get("perspective_type", "Perspective")}: {self.language_settings.get(self.first_obj.perspective_type.lower().replace(" ", "_"), self.first_obj.perspective_type)}'
1777
+ template_dict["bottom_left_3"] = f"{self._translate('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)} {self._translate('houses', 'Houses')}"
1778
+ template_dict["bottom_left_4"] = f'{self._translate("perspective_type", "Perspective")}: {self._translate(self.first_obj.perspective_type.lower().replace(" ", "_"), self.first_obj.perspective_type)}'
1432
1779
 
1433
1780
  # Moon phase section calculations
1434
1781
  template_dict["makeLunarPhase"] = ""
@@ -1437,13 +1784,13 @@ class ChartDrawer:
1437
1784
  template_dict["makeMainHousesGrid"] = draw_main_house_grid(
1438
1785
  main_subject_houses_list=first_subject_houses_list,
1439
1786
  text_color=self.chart_colors_settings["paper_0"],
1440
- house_cusp_generale_name_label=self.language_settings["cusp"],
1787
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1441
1788
  )
1442
1789
 
1443
1790
  template_dict["makeSecondaryHousesGrid"] = draw_secondary_house_grid(
1444
1791
  secondary_subject_houses_list=second_subject_houses_list,
1445
1792
  text_color=self.chart_colors_settings["paper_0"],
1446
- house_cusp_generale_name_label=self.language_settings["cusp"],
1793
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1447
1794
  )
1448
1795
 
1449
1796
  template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
@@ -1475,23 +1822,63 @@ class ChartDrawer:
1475
1822
  )
1476
1823
 
1477
1824
  # Planet grid
1825
+ first_name_label = self._truncate_name(self.first_obj.name, 18, "…") # type: ignore[union-attr]
1826
+ second_name_label = self._truncate_name(self.second_obj.name, 18, "…") # type: ignore[union-attr]
1478
1827
  template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
1479
1828
  planets_and_houses_grid_title="",
1480
- subject_name=f"{self.first_obj.name} ({self.language_settings.get('inner_wheel', 'Inner Wheel')})",
1829
+ subject_name=f"{first_name_label} ({self._translate('inner_wheel', 'Inner Wheel')})",
1481
1830
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
1482
1831
  chart_type=self.chart_type,
1483
1832
  text_color=self.chart_colors_settings["paper_0"],
1484
- celestial_point_language=self.language_settings["celestial_points"],
1833
+ celestial_point_language=self._language_model.celestial_points,
1485
1834
  )
1486
1835
  template_dict["makeSecondaryPlanetGrid"] = draw_secondary_planet_grid(
1487
1836
  planets_and_houses_grid_title="",
1488
- second_subject_name= f"{self.second_obj.name} ({self.language_settings.get('outer_wheel', 'Outer Wheel')})", # type: ignore
1837
+ second_subject_name= f"{second_name_label} ({self._translate('outer_wheel', 'Outer Wheel')})", # type: ignore
1489
1838
  second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
1490
1839
  chart_type=self.chart_type,
1491
1840
  text_color=self.chart_colors_settings["paper_0"],
1492
- celestial_point_language=self.language_settings["celestial_points"],
1841
+ celestial_point_language=self._language_model.celestial_points,
1493
1842
  )
1494
- template_dict["makeHouseComparisonGrid"] = ""
1843
+ house_comparison_factory = HouseComparisonFactory(
1844
+ first_subject=self.first_obj, # type: ignore[arg-type]
1845
+ second_subject=self.second_obj, # type: ignore[arg-type]
1846
+ active_points=self.active_points,
1847
+ )
1848
+ house_comparison = house_comparison_factory.get_house_comparison()
1849
+
1850
+ first_subject_label = self._truncate_name(self.first_obj.name, 8, "…", True) # type: ignore[union-attr]
1851
+ second_subject_label = self._truncate_name(self.second_obj.name, 8, "…", True) # type: ignore[union-attr]
1852
+ point_column_label = self._translate("point", "Point")
1853
+ comparison_label = self._translate("house_position_comparison", "House Position Comparison")
1854
+
1855
+ first_subject_grid = draw_house_comparison_grid(
1856
+ house_comparison,
1857
+ celestial_point_language=self._language_model.celestial_points,
1858
+ active_points=self.active_points,
1859
+ points_owner_subject_number=1,
1860
+ house_position_comparison_label=comparison_label,
1861
+ return_point_label=first_subject_label + " " + point_column_label,
1862
+ return_label=first_subject_label,
1863
+ radix_label=second_subject_label,
1864
+ x_position=1090,
1865
+ y_position=0,
1866
+ )
1867
+
1868
+ second_subject_grid = draw_house_comparison_grid(
1869
+ house_comparison,
1870
+ celestial_point_language=self._language_model.celestial_points,
1871
+ active_points=self.active_points,
1872
+ points_owner_subject_number=2,
1873
+ house_position_comparison_label="",
1874
+ return_point_label=second_subject_label + " " + point_column_label,
1875
+ return_label=second_subject_label,
1876
+ radix_label=first_subject_label,
1877
+ x_position=1290,
1878
+ y_position=0,
1879
+ )
1880
+
1881
+ template_dict["makeHouseComparisonGrid"] = first_subject_grid + second_subject_grid
1495
1882
 
1496
1883
  elif self.chart_type == "DualReturnChart":
1497
1884
  # Set viewbox dynamically
@@ -1533,9 +1920,16 @@ class ChartDrawer:
1533
1920
 
1534
1921
  # Aspects
1535
1922
  if self.double_chart_aspect_grid_type == "list":
1536
- title = self.language_settings.get("return_aspects", "Natal to Return Aspects")
1923
+ title = self._translate("return_aspects", "Natal to Return Aspects")
1537
1924
  template_dict["makeAspectGrid"] = ""
1538
- template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings, max_columns=7) # type: ignore[arg-type] # type: ignore[arg-type]
1925
+ template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(
1926
+ title,
1927
+ self.aspects_list,
1928
+ self.planets_settings,
1929
+ self.aspects_settings,
1930
+ max_columns=7,
1931
+ chart_height=self.height,
1932
+ ) # type: ignore[arg-type]
1539
1933
  else:
1540
1934
  template_dict["makeAspectGrid"] = ""
1541
1935
  template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
@@ -1551,17 +1945,17 @@ class ChartDrawer:
1551
1945
 
1552
1946
  # Top left section
1553
1947
  # Subject
1554
- latitude_string = convert_latitude_coordinate_to_string(self.first_obj.lat, self.language_settings["north"], self.language_settings["south"]) # type: ignore
1555
- longitude_string = convert_longitude_coordinate_to_string(self.first_obj.lng, self.language_settings["east"], self.language_settings["west"]) # type: ignore
1948
+ latitude_string = convert_latitude_coordinate_to_string(self.first_obj.lat, self._translate("north", "North"), self._translate("south", "South")) # type: ignore
1949
+ longitude_string = convert_longitude_coordinate_to_string(self.first_obj.lng, self._translate("east", "East"), self._translate("west", "West")) # type: ignore
1556
1950
 
1557
1951
  # Return
1558
- return_latitude_string = convert_latitude_coordinate_to_string(self.second_obj.lat, self.language_settings["north"], self.language_settings["south"]) # type: ignore
1559
- return_longitude_string = convert_longitude_coordinate_to_string(self.second_obj.lng, self.language_settings["east"], self.language_settings["west"]) # type: ignore
1952
+ return_latitude_string = convert_latitude_coordinate_to_string(self.second_obj.lat, self._translate("north", "North"), self._translate("south", "South")) # type: ignore
1953
+ return_longitude_string = convert_longitude_coordinate_to_string(self.second_obj.lng, self._translate("east", "East"), self._translate("west", "West")) # type: ignore
1560
1954
 
1561
1955
  if self.second_obj is not None and hasattr(self.second_obj, 'return_type') and self.second_obj.return_type == "Solar":
1562
- template_dict["top_left_0"] = f"{self.language_settings.get('solar_return', 'Solar Return')}:"
1956
+ template_dict["top_left_0"] = f"{self._translate('solar_return', 'Solar Return')}:"
1563
1957
  else:
1564
- template_dict["top_left_0"] = f"{self.language_settings.get('lunar_return', 'Lunar Return')}:"
1958
+ template_dict["top_left_0"] = f"{self._translate('lunar_return', 'Lunar Return')}:"
1565
1959
  template_dict["top_left_1"] = format_datetime_with_timezone(self.second_obj.iso_formatted_local_datetime) # type: ignore
1566
1960
  template_dict["top_left_2"] = f"{return_latitude_string} / {return_longitude_string}"
1567
1961
  template_dict["top_left_3"] = f"{self.first_obj.name}"
@@ -1570,24 +1964,24 @@ class ChartDrawer:
1570
1964
 
1571
1965
  # Bottom left section
1572
1966
  if self.first_obj.zodiac_type == "Tropic":
1573
- zodiac_info = f"{self.language_settings.get('zodiac', 'Zodiac')}: {self.language_settings.get('tropical', 'Tropical')}"
1967
+ zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1574
1968
  else:
1575
1969
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
1576
1970
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
1577
- zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
1971
+ zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
1578
1972
 
1579
1973
  template_dict["bottom_left_0"] = zodiac_info
1580
- template_dict["bottom_left_1"] = f"{self.language_settings.get('domification', 'Domification')}: {self.language_settings.get('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)}"
1974
+ template_dict["bottom_left_1"] = f"{self._translate('domification', 'Domification')}: {self._translate('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)}"
1581
1975
 
1582
1976
  # Lunar phase information (optional)
1583
1977
  if self.first_obj.lunar_phase is not None:
1584
- template_dict["bottom_left_2"] = f'{self.language_settings.get("lunation_day", "Lunation Day")}: {self.first_obj.lunar_phase.get("moon_phase", "")}'
1585
- template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get(self.first_obj.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.first_obj.lunar_phase.moon_phase_name)}'
1978
+ template_dict["bottom_left_2"] = f'{self._translate("lunation_day", "Lunation Day")}: {self.first_obj.lunar_phase.get("moon_phase", "")}'
1979
+ template_dict["bottom_left_3"] = f'{self._translate("lunar_phase", "Lunar Phase")}: {self._translate(self.first_obj.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.first_obj.lunar_phase.moon_phase_name)}'
1586
1980
  else:
1587
1981
  template_dict["bottom_left_2"] = ""
1588
1982
  template_dict["bottom_left_3"] = ""
1589
1983
 
1590
- template_dict["bottom_left_4"] = f'{self.language_settings.get("perspective_type", "Perspective")}: {self.language_settings.get(self.first_obj.perspective_type.lower().replace(" ", "_"), self.first_obj.perspective_type)}'
1984
+ template_dict["bottom_left_4"] = f'{self._translate("perspective_type", "Perspective")}: {self._translate(self.first_obj.perspective_type.lower().replace(" ", "_"), self.first_obj.perspective_type)}'
1591
1985
 
1592
1986
  # Moon phase section calculations
1593
1987
  if self.first_obj.lunar_phase is not None:
@@ -1599,13 +1993,13 @@ class ChartDrawer:
1599
1993
  template_dict["makeMainHousesGrid"] = draw_main_house_grid(
1600
1994
  main_subject_houses_list=first_subject_houses_list,
1601
1995
  text_color=self.chart_colors_settings["paper_0"],
1602
- house_cusp_generale_name_label=self.language_settings["cusp"],
1996
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1603
1997
  )
1604
1998
 
1605
1999
  template_dict["makeSecondaryHousesGrid"] = draw_secondary_house_grid(
1606
2000
  secondary_subject_houses_list=second_subject_houses_list,
1607
2001
  text_color=self.chart_colors_settings["paper_0"],
1608
- house_cusp_generale_name_label=self.language_settings["cusp"],
2002
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1609
2003
  )
1610
2004
 
1611
2005
  template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
@@ -1637,19 +2031,20 @@ class ChartDrawer:
1637
2031
  )
1638
2032
 
1639
2033
  # Planet grid
2034
+ first_name_label = self._truncate_name(self.first_obj.name)
1640
2035
  if self.second_obj is not None and hasattr(self.second_obj, 'return_type') and self.second_obj.return_type == "Solar":
1641
- first_return_grid_title = f"{self.first_obj.name} ({self.language_settings.get('inner_wheel', 'Inner Wheel')})"
1642
- second_return_grid_title = f"{self.language_settings.get('solar_return', 'Solar Return')} ({self.language_settings.get('outer_wheel', 'Outer Wheel')})"
2036
+ first_return_grid_title = f"{first_name_label} ({self._translate('inner_wheel', 'Inner Wheel')})"
2037
+ second_return_grid_title = f"{self._translate('solar_return', 'Solar Return')} ({self._translate('outer_wheel', 'Outer Wheel')})"
1643
2038
  else:
1644
- first_return_grid_title = f"{self.first_obj.name} ({self.language_settings.get('inner_wheel', 'Inner Wheel')})"
1645
- second_return_grid_title = f'{self.language_settings.get("lunar_return", "Lunar Return")} ({self.language_settings.get("outer_wheel", "Outer Wheel")})'
2039
+ first_return_grid_title = f"{first_name_label} ({self._translate('inner_wheel', 'Inner Wheel')})"
2040
+ second_return_grid_title = f'{self._translate("lunar_return", "Lunar Return")} ({self._translate("outer_wheel", "Outer Wheel")})'
1646
2041
  template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
1647
2042
  planets_and_houses_grid_title="",
1648
2043
  subject_name=first_return_grid_title,
1649
2044
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
1650
2045
  chart_type=self.chart_type,
1651
2046
  text_color=self.chart_colors_settings["paper_0"],
1652
- celestial_point_language=self.language_settings["celestial_points"],
2047
+ celestial_point_language=self._language_model.celestial_points,
1653
2048
  )
1654
2049
  template_dict["makeSecondaryPlanetGrid"] = draw_secondary_planet_grid(
1655
2050
  planets_and_houses_grid_title="",
@@ -1657,7 +2052,7 @@ class ChartDrawer:
1657
2052
  second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
1658
2053
  chart_type=self.chart_type,
1659
2054
  text_color=self.chart_colors_settings["paper_0"],
1660
- celestial_point_language=self.language_settings["celestial_points"],
2055
+ celestial_point_language=self._language_model.celestial_points,
1661
2056
  )
1662
2057
 
1663
2058
  house_comparison_factory = HouseComparisonFactory(
@@ -1669,13 +2064,13 @@ class ChartDrawer:
1669
2064
 
1670
2065
  template_dict["makeHouseComparisonGrid"] = draw_house_comparison_grid(
1671
2066
  house_comparison,
1672
- celestial_point_language=self.language_settings["celestial_points"],
2067
+ celestial_point_language=self._language_model.celestial_points,
1673
2068
  active_points=self.active_points,
1674
2069
  points_owner_subject_number=2, # The second subject is the Solar Return
1675
- house_position_comparison_label=self.language_settings.get("house_position_comparison", "House Position Comparison"),
1676
- return_point_label=self.language_settings.get("return_point", "Return Point"),
1677
- return_label=self.language_settings.get("Return", "DualReturnChart"),
1678
- radix_label=self.language_settings.get("Natal", "Natal"),
2070
+ house_position_comparison_label=self._translate("house_position_comparison", "House Position Comparison"),
2071
+ return_point_label=self._translate("return_point", "Return Point"),
2072
+ return_label=self._translate("Return", "DualReturnChart"),
2073
+ radix_label=self._translate("Natal", "Natal"),
1679
2074
  )
1680
2075
 
1681
2076
  elif self.chart_type == "SingleReturnChart":
@@ -1726,40 +2121,40 @@ class ChartDrawer:
1726
2121
  template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
1727
2122
 
1728
2123
  # Top left section
1729
- latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
1730
- longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings["east"], self.language_settings["west"])
2124
+ latitude_string = convert_latitude_coordinate_to_string(self.geolat, self._translate("north", "North"), self._translate("south", "South"))
2125
+ longitude_string = convert_longitude_coordinate_to_string(self.geolon, self._translate("east", "East"), self._translate("west", "West"))
1731
2126
 
1732
- template_dict["top_left_0"] = f'{self.language_settings["info"]}:'
2127
+ template_dict["top_left_0"] = f'{self._translate("info", "Info")}:'
1733
2128
  template_dict["top_left_1"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
1734
2129
  template_dict["top_left_2"] = f"{self.first_obj.city}, {self.first_obj.nation}"
1735
- template_dict["top_left_3"] = f"{self.language_settings['latitude']}: {latitude_string}"
1736
- template_dict["top_left_4"] = f"{self.language_settings['longitude']}: {longitude_string}"
2130
+ template_dict["top_left_3"] = f"{self._translate('latitude', 'Latitude')}: {latitude_string}"
2131
+ template_dict["top_left_4"] = f"{self._translate('longitude', 'Longitude')}: {longitude_string}"
1737
2132
 
1738
2133
  if hasattr(self.first_obj, 'return_type') and self.first_obj.return_type == "Solar":
1739
- template_dict["top_left_5"] = f"{self.language_settings['type']}: {self.language_settings.get('solar_return', 'Solar Return')}"
2134
+ template_dict["top_left_5"] = f"{self._translate('type', 'Type')}: {self._translate('solar_return', 'Solar Return')}"
1740
2135
  else:
1741
- template_dict["top_left_5"] = f"{self.language_settings['type']}: {self.language_settings.get('lunar_return', 'Lunar Return')}"
2136
+ template_dict["top_left_5"] = f"{self._translate('type', 'Type')}: {self._translate('lunar_return', 'Lunar Return')}"
1742
2137
 
1743
2138
  # Bottom left section
1744
2139
  if self.first_obj.zodiac_type == "Tropic":
1745
- zodiac_info = f"{self.language_settings.get('zodiac', 'Zodiac')}: {self.language_settings.get('tropical', 'Tropical')}"
2140
+ zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
1746
2141
  else:
1747
2142
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
1748
2143
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
1749
- zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
2144
+ zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
1750
2145
 
1751
2146
  template_dict["bottom_left_0"] = zodiac_info
1752
- template_dict["bottom_left_1"] = f"{self.language_settings.get('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)} {self.language_settings.get('houses', 'Houses')}"
2147
+ template_dict["bottom_left_1"] = f"{self._translate('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)} {self._translate('houses', 'Houses')}"
1753
2148
 
1754
2149
  # Lunar phase information (optional)
1755
2150
  if self.first_obj.lunar_phase is not None:
1756
- template_dict["bottom_left_2"] = f'{self.language_settings.get("lunation_day", "Lunation Day")}: {self.first_obj.lunar_phase.get("moon_phase", "")}'
1757
- template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get(self.first_obj.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.first_obj.lunar_phase.moon_phase_name)}'
2151
+ template_dict["bottom_left_2"] = f'{self._translate("lunation_day", "Lunation Day")}: {self.first_obj.lunar_phase.get("moon_phase", "")}'
2152
+ template_dict["bottom_left_3"] = f'{self._translate("lunar_phase", "Lunar Phase")}: {self._translate(self.first_obj.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.first_obj.lunar_phase.moon_phase_name)}'
1758
2153
  else:
1759
2154
  template_dict["bottom_left_2"] = ""
1760
2155
  template_dict["bottom_left_3"] = ""
1761
2156
 
1762
- template_dict["bottom_left_4"] = f'{self.language_settings.get("perspective_type", "Perspective")}: {self.language_settings.get(self.first_obj.perspective_type.lower().replace(" ", "_"), self.first_obj.perspective_type)}'
2157
+ template_dict["bottom_left_4"] = f'{self._translate("perspective_type", "Perspective")}: {self._translate(self.first_obj.perspective_type.lower().replace(" ", "_"), self.first_obj.perspective_type)}'
1763
2158
 
1764
2159
  # Moon phase section calculations
1765
2160
  if self.first_obj.lunar_phase is not None:
@@ -1771,7 +2166,7 @@ class ChartDrawer:
1771
2166
  template_dict["makeMainHousesGrid"] = draw_main_house_grid(
1772
2167
  main_subject_houses_list=first_subject_houses_list,
1773
2168
  text_color=self.chart_colors_settings["paper_0"],
1774
- house_cusp_generale_name_label=self.language_settings["cusp"],
2169
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1775
2170
  )
1776
2171
  template_dict["makeSecondaryHousesGrid"] = ""
1777
2172
 
@@ -1801,12 +2196,12 @@ class ChartDrawer:
1801
2196
  )
1802
2197
 
1803
2198
  template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
1804
- planets_and_houses_grid_title=self.language_settings["planets_and_house"],
2199
+ planets_and_houses_grid_title=self._translate("planets_and_house", "Points for"),
1805
2200
  subject_name=self.first_obj.name,
1806
2201
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
1807
2202
  chart_type=self.chart_type,
1808
2203
  text_color=self.chart_colors_settings["paper_0"],
1809
- celestial_point_language=self.language_settings["celestial_points"],
2204
+ celestial_point_language=self._language_model.celestial_points,
1810
2205
  )
1811
2206
  template_dict["makeSecondaryPlanetGrid"] = ""
1812
2207
  template_dict["makeHouseComparisonGrid"] = ""