kerykeion 5.0.0b2__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 (29) 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 +524 -203
  7. kerykeion/charts/charts_utils.py +416 -253
  8. kerykeion/charts/templates/aspect_grid_only.xml +269 -312
  9. kerykeion/charts/templates/chart.xml +248 -304
  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/kr_literals.py +1 -1
  14. kerykeion/settings/__init__.py +16 -2
  15. kerykeion/settings/chart_defaults.py +444 -0
  16. kerykeion/settings/config_constants.py +0 -5
  17. kerykeion/settings/kerykeion_settings.py +31 -74
  18. kerykeion/settings/translation_strings.py +1479 -0
  19. kerykeion/settings/translations.py +74 -0
  20. kerykeion/transits_time_range_factory.py +10 -1
  21. {kerykeion-5.0.0b2.dist-info → kerykeion-5.0.0b4.dist-info}/METADATA +304 -204
  22. {kerykeion-5.0.0b2.dist-info → kerykeion-5.0.0b4.dist-info}/RECORD +24 -25
  23. kerykeion/settings/kr.config.json +0 -1474
  24. kerykeion/settings/legacy/__init__.py +0 -0
  25. kerykeion/settings/legacy/legacy_celestial_points_settings.py +0 -299
  26. kerykeion/settings/legacy/legacy_chart_aspects_settings.py +0 -71
  27. kerykeion/settings/legacy/legacy_color_settings.py +0 -42
  28. {kerykeion-5.0.0b2.dist-info → kerykeion-5.0.0b4.dist-info}/WHEEL +0 -0
  29. {kerykeion-5.0.0b2.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,14 @@ 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
+
164
176
  _BASE_VERTICAL_OFFSETS = {
165
177
  "wheel": 50,
166
178
  "grid": 0,
@@ -185,7 +197,6 @@ class ChartDrawer:
185
197
  first_obj: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel]
186
198
  second_obj: Union[AstrologicalSubjectModel, PlanetReturnModel, None]
187
199
  chart_type: ChartType
188
- new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
189
200
  theme: Union[KerykeionChartTheme, None]
190
201
  double_chart_aspect_grid_type: Literal["list", "table"]
191
202
  chart_language: KerykeionChartLanguage
@@ -194,6 +205,8 @@ class ChartDrawer:
194
205
  transparent_background: bool
195
206
  external_view: bool
196
207
  custom_title: Union[str, None]
208
+ _language_model: KerykeionLanguageModel
209
+ _fallback_language_model: KerykeionLanguageModel
197
210
 
198
211
  # Internal properties
199
212
  fire: float
@@ -219,10 +232,10 @@ class ChartDrawer:
219
232
  self,
220
233
  chart_data: "ChartDataModel",
221
234
  *,
222
- new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] = None,
223
235
  theme: Union[KerykeionChartTheme, None] = "classic",
224
236
  double_chart_aspect_grid_type: Literal["list", "table"] = "list",
225
237
  chart_language: KerykeionChartLanguage = "EN",
238
+ language_pack: Optional[Mapping[str, Any]] = None,
226
239
  external_view: bool = False,
227
240
  transparent_background: bool = False,
228
241
  colors_settings: dict = DEFAULT_CHART_COLORS,
@@ -239,14 +252,16 @@ class ChartDrawer:
239
252
  chart_data (ChartDataModel):
240
253
  Pre-computed chart data from ChartDataFactory containing all subjects,
241
254
  aspects, element/quality distributions, and other analytical data.
242
- new_settings_file (Path, dict, or KerykeionSettingsModel, optional):
243
- Custom settings source for chart colors, fonts, and aspects.
244
255
  theme (KerykeionChartTheme or None, optional):
245
256
  CSS theme to apply; None for default styling.
246
257
  double_chart_aspect_grid_type (Literal['list','table'], optional):
247
258
  Layout style for double-chart aspect grids ('list' or 'table').
248
259
  chart_language (KerykeionChartLanguage, optional):
249
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.
250
265
  external_view (bool, optional):
251
266
  Whether to use external visualization (planets on outer ring) for single-subject charts. Defaults to False.
252
267
  transparent_background (bool, optional):
@@ -257,14 +272,13 @@ class ChartDrawer:
257
272
  # --------------------
258
273
  # COMMON INITIALIZATION
259
274
  # --------------------
260
- self.new_settings_file = new_settings_file
261
275
  self.chart_language = chart_language
262
276
  self.double_chart_aspect_grid_type = double_chart_aspect_grid_type
263
277
  self.transparent_background = transparent_background
264
278
  self.external_view = external_view
265
- self.chart_colors_settings = colors_settings
266
- self.planets_settings = celestial_points_settings
267
- 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]
268
282
  self.custom_title = custom_title
269
283
  self.auto_size = auto_size
270
284
  self._padding = padding
@@ -287,7 +301,7 @@ class ChartDrawer:
287
301
  self.second_obj = getattr(chart_data, 'second_subject')
288
302
 
289
303
  # Load settings
290
- self.parse_json_settings(new_settings_file)
304
+ self._load_language_settings(language_pack)
291
305
 
292
306
  # Default radius for all charts
293
307
  self.main_radius = 240
@@ -308,10 +322,18 @@ class ChartDrawer:
308
322
 
309
323
  # Set available celestial points
310
324
  available_celestial_points_names = [body["name"].lower() for body in self.available_planets_setting]
311
- self.available_kerykeion_celestial_points = []
312
- for body in available_celestial_points_names:
313
- if hasattr(self.first_obj, body):
314
- 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
+ )
315
337
 
316
338
  # ------------------------
317
339
  # CHART TYPE SPECIFIC SETUP FROM CHART DATA
@@ -364,9 +386,6 @@ class ChartDrawer:
364
386
  # Extract aspects from pre-computed chart data
365
387
  self.aspects_list = chart_data.aspects.relevant_aspects
366
388
 
367
- # Secondary subject available points
368
- self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
369
-
370
389
  # Screen size
371
390
  self.height = self._DEFAULT_HEIGHT
372
391
  if self.double_chart_aspect_grid_type == "table":
@@ -388,12 +407,9 @@ class ChartDrawer:
388
407
  # Extract aspects from pre-computed chart data
389
408
  self.aspects_list = chart_data.aspects.relevant_aspects
390
409
 
391
- # Secondary subject available points
392
- self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
393
-
394
410
  # Screen size
395
411
  self.height = self._DEFAULT_HEIGHT
396
- self.width = self._DEFAULT_FULL_WIDTH
412
+ self.width = self._DEFAULT_SYNASTRY_WIDTH
397
413
 
398
414
  # Get location and coordinates
399
415
  self.location, self.geolat, self.geolon = self._get_location_info()
@@ -409,9 +425,6 @@ class ChartDrawer:
409
425
  # Extract aspects from pre-computed chart data
410
426
  self.aspects_list = chart_data.aspects.relevant_aspects
411
427
 
412
- # Secondary subject available points
413
- self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
414
-
415
428
  # Screen size
416
429
  self.height = self._DEFAULT_HEIGHT
417
430
  self.width = self._DEFAULT_ULTRA_WIDE_WIDTH
@@ -462,17 +475,11 @@ class ChartDrawer:
462
475
 
463
476
  self.set_up_theme(theme)
464
477
 
465
- # Optionally expand width dynamically to fit content
466
- if self.auto_size:
467
- try:
468
- required_width = self._estimate_required_width_full()
469
- if required_width > self.width:
470
- self.width = required_width
471
- except Exception as e:
472
- # Keep default on any unexpected issue; do not break rendering
473
- logging.debug(f"Auto-size width calculation failed: {e}")
474
-
475
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.
481
+ if self.auto_size:
482
+ self._update_width_to_content()
476
483
 
477
484
  def _count_active_planets(self) -> int:
478
485
  """Return number of active celestial points in the current chart."""
@@ -485,6 +492,15 @@ class ChartDrawer:
485
492
  offsets = self._BASE_VERTICAL_OFFSETS.copy()
486
493
 
487
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
+
488
504
  if active_points_count <= 20:
489
505
  self.height = max(self.height, minimum_height)
490
506
  self._vertical_offsets = offsets
@@ -515,6 +531,135 @@ class ChartDrawer:
515
531
 
516
532
  self._vertical_offsets = offsets
517
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
+
518
663
  def _dynamic_viewbox(self) -> str:
519
664
  """Return the viewBox string based on current width/height."""
520
665
  return f"0 0 {int(self.width)} {int(self.height)}"
@@ -604,14 +749,10 @@ class ChartDrawer:
604
749
  if self.chart_type in ("Transit", "Synastry", "DualReturnChart"):
605
750
  # Double-chart aspects placement
606
751
  if self.double_chart_aspect_grid_type == "list":
607
- # Columnar list placed at translate(565,273), ~100-110px per column, 14 aspects per column
608
- aspects_per_column = 14
609
752
  total_aspects = len(self.aspects_list) if hasattr(self, "aspects_list") else 0
610
- columns = max((total_aspects + aspects_per_column - 1) // aspects_per_column, 1)
611
- # Respect the max columns cap used in rendering: DualReturn=7, others=6
612
- max_cols_cap = 7
613
- columns = min(columns, max_cols_cap)
614
- 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)
615
756
  extents.append(aspect_list_right)
616
757
  else:
617
758
  # Grid table placed with x_indent ~550, width ~ 14px per cell across n_active+1
@@ -626,6 +767,9 @@ class ChartDrawer:
626
767
  # Secondary houses grid default x ~ 1015
627
768
  secondary_houses_grid_right = 1015 + 120
628
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])
629
773
 
630
774
  if self.chart_type == "Transit":
631
775
  # House comparison grid at x ~ 1030
@@ -640,6 +784,84 @@ class ChartDrawer:
640
784
  # Conservative safety padding
641
785
  return int(max(extents) + self._padding)
642
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
+
643
865
  def _get_location_info(self) -> tuple[str, float, float]:
644
866
  """
645
867
  Determine location information based on chart type and subjects.
@@ -687,17 +909,31 @@ class ChartDrawer:
687
909
  with open(theme_dir / f"{theme}.css", "r") as f:
688
910
  self.color_style_tag = f.read()
689
911
 
690
- def parse_json_settings(self, settings_file_or_dict: Union[Path, dict, KerykeionSettingsModel, None]) -> None:
691
- """
692
- Load and parse chart configuration settings.
693
-
694
- Args:
695
- settings_file_or_dict (Path, dict, or KerykeionSettingsModel):
696
- Source for custom chart settings.
697
- """
698
- settings = get_settings(settings_file_or_dict)
699
-
700
- 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)
701
937
 
702
938
  def _draw_zodiac_circle_slices(self, r):
703
939
  """
@@ -774,7 +1010,7 @@ class ChartDrawer:
774
1010
  )
775
1011
  return out
776
1012
 
777
- 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:
778
1014
  """
779
1015
  Truncate a name if it's too long, preserving readability.
780
1016
 
@@ -785,9 +1021,13 @@ class ChartDrawer:
785
1021
  Returns:
786
1022
  str: Truncated name with ellipsis if needed
787
1023
  """
1024
+ if truncate_at_space:
1025
+ name = name.split(" ")[0]
1026
+
788
1027
  if len(name) <= max_length:
789
1028
  return name
790
- return name[:max_length-1] + "…"
1029
+
1030
+ return name[:max_length-1] + ellipsis_symbol
791
1031
 
792
1032
  def _get_chart_title(self) -> str:
793
1033
  """
@@ -805,52 +1045,49 @@ class ChartDrawer:
805
1045
 
806
1046
  # Generate default title based on chart type
807
1047
  if self.chart_type == "Natal":
808
- natal_label = self.language_settings.get("birth_chart", "Natal")
1048
+ natal_label = self._translate("birth_chart", "Natal")
809
1049
  truncated_name = self._truncate_name(self.first_obj.name)
810
1050
  return f'{truncated_name} - {natal_label}'
811
1051
 
812
1052
  elif self.chart_type == "Composite":
813
- composite_label = self.language_settings.get("composite_chart", "Composite")
814
- and_word = self.language_settings.get("and_word", "&")
1053
+ composite_label = self._translate("composite_chart", "Composite")
1054
+ and_word = self._translate("and_word", "&")
815
1055
  name1 = self._truncate_name(self.first_obj.first_subject.name) # type: ignore
816
1056
  name2 = self._truncate_name(self.first_obj.second_subject.name) # type: ignore
817
1057
  return f"{composite_label}: {name1} {and_word} {name2}"
818
1058
 
819
1059
  elif self.chart_type == "Transit":
820
- transit_label = self.language_settings.get("transits", "Transits")
821
- from datetime import datetime
1060
+ transit_label = self._translate("transits", "Transits")
822
1061
  date_obj = datetime.fromisoformat(self.second_obj.iso_formatted_local_datetime) # type: ignore
823
1062
  date_str = date_obj.strftime("%d/%m/%y")
824
1063
  truncated_name = self._truncate_name(self.first_obj.name)
825
1064
  return f"{truncated_name} - {transit_label} {date_str}"
826
1065
 
827
1066
  elif self.chart_type == "Synastry":
828
- synastry_label = self.language_settings.get("synastry_chart", "Synastry")
829
- and_word = self.language_settings.get("and_word", "&")
1067
+ synastry_label = self._translate("synastry_chart", "Synastry")
1068
+ and_word = self._translate("and_word", "&")
830
1069
  name1 = self._truncate_name(self.first_obj.name)
831
1070
  name2 = self._truncate_name(self.second_obj.name) # type: ignore
832
1071
  return f"{synastry_label}: {name1} {and_word} {name2}"
833
1072
 
834
1073
  elif self.chart_type == "DualReturnChart":
835
- from datetime import datetime
836
1074
  year = datetime.fromisoformat(self.second_obj.iso_formatted_local_datetime).year # type: ignore
837
1075
  truncated_name = self._truncate_name(self.first_obj.name)
838
1076
  if self.second_obj is not None and isinstance(self.second_obj, PlanetReturnModel) and self.second_obj.return_type == "Solar":
839
- solar_label = self.language_settings.get("solar_return", "Solar")
1077
+ solar_label = self._translate("solar_return", "Solar")
840
1078
  return f"{truncated_name} - {solar_label} {year}"
841
1079
  else:
842
- lunar_label = self.language_settings.get("lunar_return", "Lunar")
1080
+ lunar_label = self._translate("lunar_return", "Lunar")
843
1081
  return f"{truncated_name} - {lunar_label} {year}"
844
1082
 
845
1083
  elif self.chart_type == "SingleReturnChart":
846
- from datetime import datetime
847
1084
  year = datetime.fromisoformat(self.first_obj.iso_formatted_local_datetime).year # type: ignore
848
1085
  truncated_name = self._truncate_name(self.first_obj.name)
849
1086
  if isinstance(self.first_obj, PlanetReturnModel) and self.first_obj.return_type == "Solar":
850
- solar_label = self.language_settings.get("solar_return", "Solar")
1087
+ solar_label = self._translate("solar_return", "Solar")
851
1088
  return f"{truncated_name} - {solar_label} {year}"
852
1089
  else:
853
- lunar_label = self.language_settings.get("lunar_return", "Lunar")
1090
+ lunar_label = self._translate("lunar_return", "Lunar")
854
1091
  return f"{truncated_name} - {lunar_label} {year}"
855
1092
 
856
1093
  # Fallback for unknown chart types
@@ -929,11 +1166,11 @@ class ChartDrawer:
929
1166
  water_percentage = element_percentages["water"]
930
1167
 
931
1168
  # Element Percentages
932
- template_dict["elements_string"] = f"{self.language_settings.get('elements', 'Elements')}:"
933
- template_dict["fire_string"] = f"{self.language_settings['fire']} {fire_percentage}%"
934
- template_dict["earth_string"] = f"{self.language_settings['earth']} {earth_percentage}%"
935
- template_dict["air_string"] = f"{self.language_settings['air']} {air_percentage}%"
936
- 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}%"
937
1174
 
938
1175
 
939
1176
  # Qualities Percentages
@@ -944,10 +1181,10 @@ class ChartDrawer:
944
1181
  fixed_percentage = quality_percentages["fixed"]
945
1182
  mutable_percentage = quality_percentages["mutable"]
946
1183
 
947
- template_dict["qualities_string"] = f"{self.language_settings.get('qualities', 'Qualities')}:"
948
- template_dict["cardinal_string"] = f"{self.language_settings.get('cardinal', 'Cardinal')} {cardinal_percentage}%"
949
- template_dict["fixed_string"] = f"{self.language_settings.get('fixed', 'Fixed')} {fixed_percentage}%"
950
- 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}%"
951
1188
 
952
1189
  # Get houses list for main subject
953
1190
  first_subject_houses_list = get_houses_list(self.first_obj)
@@ -1007,37 +1244,60 @@ class ChartDrawer:
1007
1244
  template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
1008
1245
 
1009
1246
  # Top left section
1010
- latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
1011
- 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
+ )
1012
1257
 
1013
- template_dict["top_left_0"] = f'{self.language_settings.get("location", "Location")}:'
1258
+ template_dict["top_left_0"] = f'{self._translate("location", "Location")}:'
1014
1259
  template_dict["top_left_1"] = f"{self.first_obj.city}, {self.first_obj.nation}"
1015
- template_dict["top_left_2"] = f"{self.language_settings['latitude']}: {latitude_string}"
1016
- 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}"
1017
1262
  template_dict["top_left_4"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
1018
- localized_weekday = self.language_settings.get('weekdays', {}).get(self.first_obj.day_of_week, self.first_obj.day_of_week) # type: ignore
1019
- 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
1020
1268
 
1021
1269
  # Bottom left section
1022
1270
  if self.first_obj.zodiac_type == "Tropic":
1023
- 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')}"
1024
1272
  else:
1025
1273
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
1026
1274
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
1027
- zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
1275
+ zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
1028
1276
 
1029
1277
  template_dict["bottom_left_0"] = zodiac_info
1030
- 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
+ )
1031
1282
 
1032
1283
  # Lunar phase information (optional)
1033
1284
  if self.first_obj.lunar_phase is not None:
1034
- template_dict["bottom_left_2"] = f'{self.language_settings.get("lunation_day", "Lunation Day")}: {self.first_obj.lunar_phase.get("moon_phase", "")}'
1035
- 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
+ )
1036
1293
  else:
1037
1294
  template_dict["bottom_left_2"] = ""
1038
1295
  template_dict["bottom_left_3"] = ""
1039
1296
 
1040
- 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
+ )
1041
1301
 
1042
1302
  # Moon phase section calculations
1043
1303
  if self.first_obj.lunar_phase is not None:
@@ -1049,7 +1309,7 @@ class ChartDrawer:
1049
1309
  template_dict["makeMainHousesGrid"] = draw_main_house_grid(
1050
1310
  main_subject_houses_list=first_subject_houses_list,
1051
1311
  text_color=self.chart_colors_settings["paper_0"],
1052
- house_cusp_generale_name_label=self.language_settings["cusp"],
1312
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1053
1313
  )
1054
1314
  template_dict["makeSecondaryHousesGrid"] = ""
1055
1315
 
@@ -1079,12 +1339,12 @@ class ChartDrawer:
1079
1339
  )
1080
1340
 
1081
1341
  template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
1082
- planets_and_houses_grid_title=self.language_settings["planets_and_house"],
1342
+ planets_and_houses_grid_title=self._translate("planets_and_house", "Points for"),
1083
1343
  subject_name=self.first_obj.name,
1084
1344
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
1085
1345
  chart_type=self.chart_type,
1086
1346
  text_color=self.chart_colors_settings["paper_0"],
1087
- celestial_point_language=self.language_settings["celestial_points"],
1347
+ celestial_point_language=self._language_model.celestial_points,
1088
1348
  )
1089
1349
  template_dict["makeSecondaryPlanetGrid"] = ""
1090
1350
  template_dict["makeHouseComparisonGrid"] = ""
@@ -1140,25 +1400,25 @@ class ChartDrawer:
1140
1400
  # First subject
1141
1401
  latitude = convert_latitude_coordinate_to_string(
1142
1402
  self.first_obj.first_subject.lat, # type: ignore
1143
- self.language_settings["north_letter"],
1144
- self.language_settings["south_letter"],
1403
+ self._translate("north_letter", "N"),
1404
+ self._translate("south_letter", "S"),
1145
1405
  )
1146
1406
  longitude = convert_longitude_coordinate_to_string(
1147
1407
  self.first_obj.first_subject.lng, # type: ignore
1148
- self.language_settings["east_letter"],
1149
- self.language_settings["west_letter"],
1408
+ self._translate("east_letter", "E"),
1409
+ self._translate("west_letter", "W"),
1150
1410
  )
1151
1411
 
1152
1412
  # Second subject
1153
1413
  latitude_string = convert_latitude_coordinate_to_string(
1154
1414
  self.first_obj.second_subject.lat, # type: ignore
1155
- self.language_settings["north_letter"],
1156
- self.language_settings["south_letter"],
1415
+ self._translate("north_letter", "N"),
1416
+ self._translate("south_letter", "S"),
1157
1417
  )
1158
1418
  longitude_string = convert_longitude_coordinate_to_string(
1159
1419
  self.first_obj.second_subject.lng, # type: ignore
1160
- self.language_settings["east_letter"],
1161
- self.language_settings["west_letter"],
1420
+ self._translate("east_letter", "E"),
1421
+ self._translate("west_letter", "W"),
1162
1422
  )
1163
1423
 
1164
1424
  template_dict["top_left_0"] = f"{self.first_obj.first_subject.name}" # type: ignore
@@ -1170,16 +1430,16 @@ class ChartDrawer:
1170
1430
 
1171
1431
  # Bottom left section
1172
1432
  if self.first_obj.zodiac_type == "Tropic":
1173
- 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')}"
1174
1434
  else:
1175
1435
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
1176
1436
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
1177
- zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
1437
+ zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
1178
1438
 
1179
1439
  template_dict["bottom_left_0"] = zodiac_info
1180
- 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')}"
1181
- template_dict["bottom_left_2"] = f'{self.language_settings.get("perspective_type", "Perspective")}: {self.first_obj.first_subject.perspective_type}' # type: ignore
1182
- 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")}'
1183
1443
  template_dict["bottom_left_4"] = ""
1184
1444
 
1185
1445
  # Moon phase section calculations
@@ -1192,7 +1452,7 @@ class ChartDrawer:
1192
1452
  template_dict["makeMainHousesGrid"] = draw_main_house_grid(
1193
1453
  main_subject_houses_list=first_subject_houses_list,
1194
1454
  text_color=self.chart_colors_settings["paper_0"],
1195
- house_cusp_generale_name_label=self.language_settings["cusp"],
1455
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1196
1456
  )
1197
1457
  template_dict["makeSecondaryHousesGrid"] = ""
1198
1458
 
@@ -1221,15 +1481,19 @@ class ChartDrawer:
1221
1481
  external_view=self.external_view,
1222
1482
  )
1223
1483
 
1224
- 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
1225
1489
 
1226
1490
  template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
1227
- planets_and_houses_grid_title=self.language_settings["planets_and_house"],
1491
+ planets_and_houses_grid_title=self._translate("planets_and_house", "Points for"),
1228
1492
  subject_name=subject_name,
1229
1493
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
1230
1494
  chart_type=self.chart_type,
1231
1495
  text_color=self.chart_colors_settings["paper_0"],
1232
- celestial_point_language=self.language_settings["celestial_points"],
1496
+ celestial_point_language=self._language_model.celestial_points,
1233
1497
  )
1234
1498
  template_dict["makeSecondaryPlanetGrid"] = ""
1235
1499
  template_dict["makeHouseComparisonGrid"] = ""
@@ -1288,9 +1552,15 @@ class ChartDrawer:
1288
1552
 
1289
1553
  # Aspects
1290
1554
  if self.double_chart_aspect_grid_type == "list":
1291
- 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")}'
1292
1556
  template_dict["makeAspectGrid"] = ""
1293
- 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]
1294
1564
  else:
1295
1565
  template_dict["makeAspectGrid"] = ""
1296
1566
  template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
@@ -1304,36 +1574,36 @@ class ChartDrawer:
1304
1574
  template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
1305
1575
 
1306
1576
  # Top left section
1307
- latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
1308
- 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"))
1309
1579
 
1310
- 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}"
1311
1581
  template_dict["top_left_1"] = f"{format_location_string(self.first_obj.city)}, {self.first_obj.nation}" # type: ignore
1312
1582
  template_dict["top_left_2"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
1313
- template_dict["top_left_3"] = f"{self.language_settings['latitude']}: {latitude_string}"
1314
- template_dict["top_left_4"] = f"{self.language_settings['longitude']}: {longitude_string}"
1315
- 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)}"
1316
1586
 
1317
1587
  # Bottom left section
1318
1588
  if self.first_obj.zodiac_type == "Tropic":
1319
- 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')}"
1320
1590
  else:
1321
1591
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
1322
1592
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
1323
- zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
1593
+ zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
1324
1594
 
1325
1595
  template_dict["bottom_left_0"] = zodiac_info
1326
- 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)}"
1327
1597
 
1328
1598
  # Lunar phase information from second object (Transit) (optional)
1329
1599
  if self.second_obj is not None and hasattr(self.second_obj, 'lunar_phase') and self.second_obj.lunar_phase is not None:
1330
- template_dict["bottom_left_2"] = f'{self.language_settings.get("lunation_day", "Lunation Day")}: {self.second_obj.lunar_phase.get("moon_phase", "")}' # type: ignore
1331
- 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)}'
1332
1602
  else:
1333
1603
  template_dict["bottom_left_2"] = ""
1334
1604
  template_dict["bottom_left_3"] = ""
1335
1605
 
1336
- 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
1337
1607
 
1338
1608
  # Moon phase section calculations - use first_obj for visualization
1339
1609
  if self.first_obj.lunar_phase is not None:
@@ -1345,12 +1615,12 @@ class ChartDrawer:
1345
1615
  template_dict["makeMainHousesGrid"] = draw_main_house_grid(
1346
1616
  main_subject_houses_list=first_subject_houses_list,
1347
1617
  text_color=self.chart_colors_settings["paper_0"],
1348
- house_cusp_generale_name_label=self.language_settings["cusp"],
1618
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1349
1619
  )
1350
1620
  # template_dict["makeSecondaryHousesGrid"] = draw_secondary_house_grid(
1351
1621
  # secondary_subject_houses_list=second_subject_houses_list,
1352
1622
  # text_color=self.chart_colors_settings["paper_0"],
1353
- # house_cusp_generale_name_label=self.language_settings["cusp"],
1623
+ # house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1354
1624
  # )
1355
1625
  template_dict["makeSecondaryHousesGrid"] = ""
1356
1626
 
@@ -1383,15 +1653,17 @@ class ChartDrawer:
1383
1653
  )
1384
1654
 
1385
1655
  # Planet grids
1386
- first_return_grid_title = f"{self.first_obj.name} ({self.language_settings.get('inner_wheel', 'Inner Wheel')})"
1387
- 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')})"
1388
1660
  template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
1389
1661
  planets_and_houses_grid_title="",
1390
1662
  subject_name=first_return_grid_title,
1391
1663
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
1392
1664
  chart_type=self.chart_type,
1393
1665
  text_color=self.chart_colors_settings["paper_0"],
1394
- celestial_point_language=self.language_settings["celestial_points"],
1666
+ celestial_point_language=self._language_model.celestial_points,
1395
1667
  )
1396
1668
 
1397
1669
  template_dict["makeSecondaryPlanetGrid"] = draw_secondary_planet_grid(
@@ -1400,7 +1672,7 @@ class ChartDrawer:
1400
1672
  second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
1401
1673
  chart_type=self.chart_type,
1402
1674
  text_color=self.chart_colors_settings["paper_0"],
1403
- celestial_point_language=self.language_settings["celestial_points"],
1675
+ celestial_point_language=self._language_model.celestial_points,
1404
1676
  )
1405
1677
 
1406
1678
  # House comparison grid
@@ -1413,12 +1685,12 @@ class ChartDrawer:
1413
1685
 
1414
1686
  template_dict["makeHouseComparisonGrid"] = draw_single_house_comparison_grid(
1415
1687
  house_comparison,
1416
- celestial_point_language=self.language_settings.get("celestial_points", "Celestial Points"),
1688
+ celestial_point_language=self._language_model.celestial_points,
1417
1689
  active_points=self.active_points,
1418
1690
  points_owner_subject_number=2, # The second subject is the Transit
1419
- house_position_comparison_label=self.language_settings.get("house_position_comparison", "House Position Comparison"),
1420
- return_point_label=self.language_settings.get("transit_point", "Transit Point"),
1421
- 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"),
1422
1694
  x_position=980,
1423
1695
  )
1424
1696
 
@@ -1464,10 +1736,11 @@ class ChartDrawer:
1464
1736
  if self.double_chart_aspect_grid_type == "list":
1465
1737
  template_dict["makeAspectGrid"] = ""
1466
1738
  template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(
1467
- 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]
1468
1740
  self.aspects_list,
1469
1741
  self.planets_settings, # type: ignore[arg-type]
1470
- self.aspects_settings # type: ignore[arg-type]
1742
+ self.aspects_settings, # type: ignore[arg-type]
1743
+ chart_height=self.height,
1471
1744
  )
1472
1745
  else:
1473
1746
  template_dict["makeAspectGrid"] = ""
@@ -1491,18 +1764,18 @@ class ChartDrawer:
1491
1764
 
1492
1765
  # Bottom left section
1493
1766
  if self.first_obj.zodiac_type == "Tropic":
1494
- 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')}"
1495
1768
  else:
1496
1769
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
1497
1770
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
1498
- zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
1771
+ zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
1499
1772
 
1500
1773
  template_dict["bottom_left_0"] = ""
1501
1774
  # FIXME!
1502
1775
  template_dict["bottom_left_1"] = "" # f"Compatibility Score: {16}/44" # type: ignore
1503
1776
  template_dict["bottom_left_2"] = zodiac_info
1504
- 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')}"
1505
- 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)}'
1506
1779
 
1507
1780
  # Moon phase section calculations
1508
1781
  template_dict["makeLunarPhase"] = ""
@@ -1511,13 +1784,13 @@ class ChartDrawer:
1511
1784
  template_dict["makeMainHousesGrid"] = draw_main_house_grid(
1512
1785
  main_subject_houses_list=first_subject_houses_list,
1513
1786
  text_color=self.chart_colors_settings["paper_0"],
1514
- house_cusp_generale_name_label=self.language_settings["cusp"],
1787
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1515
1788
  )
1516
1789
 
1517
1790
  template_dict["makeSecondaryHousesGrid"] = draw_secondary_house_grid(
1518
1791
  secondary_subject_houses_list=second_subject_houses_list,
1519
1792
  text_color=self.chart_colors_settings["paper_0"],
1520
- house_cusp_generale_name_label=self.language_settings["cusp"],
1793
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1521
1794
  )
1522
1795
 
1523
1796
  template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
@@ -1549,23 +1822,63 @@ class ChartDrawer:
1549
1822
  )
1550
1823
 
1551
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]
1552
1827
  template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
1553
1828
  planets_and_houses_grid_title="",
1554
- 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')})",
1555
1830
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
1556
1831
  chart_type=self.chart_type,
1557
1832
  text_color=self.chart_colors_settings["paper_0"],
1558
- celestial_point_language=self.language_settings["celestial_points"],
1833
+ celestial_point_language=self._language_model.celestial_points,
1559
1834
  )
1560
1835
  template_dict["makeSecondaryPlanetGrid"] = draw_secondary_planet_grid(
1561
1836
  planets_and_houses_grid_title="",
1562
- 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
1563
1838
  second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
1564
1839
  chart_type=self.chart_type,
1565
1840
  text_color=self.chart_colors_settings["paper_0"],
1566
- celestial_point_language=self.language_settings["celestial_points"],
1841
+ celestial_point_language=self._language_model.celestial_points,
1567
1842
  )
1568
- 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
1569
1882
 
1570
1883
  elif self.chart_type == "DualReturnChart":
1571
1884
  # Set viewbox dynamically
@@ -1607,9 +1920,16 @@ class ChartDrawer:
1607
1920
 
1608
1921
  # Aspects
1609
1922
  if self.double_chart_aspect_grid_type == "list":
1610
- title = self.language_settings.get("return_aspects", "Natal to Return Aspects")
1923
+ title = self._translate("return_aspects", "Natal to Return Aspects")
1611
1924
  template_dict["makeAspectGrid"] = ""
1612
- 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]
1613
1933
  else:
1614
1934
  template_dict["makeAspectGrid"] = ""
1615
1935
  template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
@@ -1625,17 +1945,17 @@ class ChartDrawer:
1625
1945
 
1626
1946
  # Top left section
1627
1947
  # Subject
1628
- latitude_string = convert_latitude_coordinate_to_string(self.first_obj.lat, self.language_settings["north"], self.language_settings["south"]) # type: ignore
1629
- 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
1630
1950
 
1631
1951
  # Return
1632
- return_latitude_string = convert_latitude_coordinate_to_string(self.second_obj.lat, self.language_settings["north"], self.language_settings["south"]) # type: ignore
1633
- 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
1634
1954
 
1635
1955
  if self.second_obj is not None and hasattr(self.second_obj, 'return_type') and self.second_obj.return_type == "Solar":
1636
- 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')}:"
1637
1957
  else:
1638
- 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')}:"
1639
1959
  template_dict["top_left_1"] = format_datetime_with_timezone(self.second_obj.iso_formatted_local_datetime) # type: ignore
1640
1960
  template_dict["top_left_2"] = f"{return_latitude_string} / {return_longitude_string}"
1641
1961
  template_dict["top_left_3"] = f"{self.first_obj.name}"
@@ -1644,24 +1964,24 @@ class ChartDrawer:
1644
1964
 
1645
1965
  # Bottom left section
1646
1966
  if self.first_obj.zodiac_type == "Tropic":
1647
- 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')}"
1648
1968
  else:
1649
1969
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
1650
1970
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
1651
- zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
1971
+ zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
1652
1972
 
1653
1973
  template_dict["bottom_left_0"] = zodiac_info
1654
- 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)}"
1655
1975
 
1656
1976
  # Lunar phase information (optional)
1657
1977
  if self.first_obj.lunar_phase is not None:
1658
- template_dict["bottom_left_2"] = f'{self.language_settings.get("lunation_day", "Lunation Day")}: {self.first_obj.lunar_phase.get("moon_phase", "")}'
1659
- 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)}'
1660
1980
  else:
1661
1981
  template_dict["bottom_left_2"] = ""
1662
1982
  template_dict["bottom_left_3"] = ""
1663
1983
 
1664
- 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)}'
1665
1985
 
1666
1986
  # Moon phase section calculations
1667
1987
  if self.first_obj.lunar_phase is not None:
@@ -1673,13 +1993,13 @@ class ChartDrawer:
1673
1993
  template_dict["makeMainHousesGrid"] = draw_main_house_grid(
1674
1994
  main_subject_houses_list=first_subject_houses_list,
1675
1995
  text_color=self.chart_colors_settings["paper_0"],
1676
- house_cusp_generale_name_label=self.language_settings["cusp"],
1996
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1677
1997
  )
1678
1998
 
1679
1999
  template_dict["makeSecondaryHousesGrid"] = draw_secondary_house_grid(
1680
2000
  secondary_subject_houses_list=second_subject_houses_list,
1681
2001
  text_color=self.chart_colors_settings["paper_0"],
1682
- house_cusp_generale_name_label=self.language_settings["cusp"],
2002
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1683
2003
  )
1684
2004
 
1685
2005
  template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
@@ -1711,19 +2031,20 @@ class ChartDrawer:
1711
2031
  )
1712
2032
 
1713
2033
  # Planet grid
2034
+ first_name_label = self._truncate_name(self.first_obj.name)
1714
2035
  if self.second_obj is not None and hasattr(self.second_obj, 'return_type') and self.second_obj.return_type == "Solar":
1715
- first_return_grid_title = f"{self.first_obj.name} ({self.language_settings.get('inner_wheel', 'Inner Wheel')})"
1716
- 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')})"
1717
2038
  else:
1718
- first_return_grid_title = f"{self.first_obj.name} ({self.language_settings.get('inner_wheel', 'Inner Wheel')})"
1719
- 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")})'
1720
2041
  template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
1721
2042
  planets_and_houses_grid_title="",
1722
2043
  subject_name=first_return_grid_title,
1723
2044
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
1724
2045
  chart_type=self.chart_type,
1725
2046
  text_color=self.chart_colors_settings["paper_0"],
1726
- celestial_point_language=self.language_settings["celestial_points"],
2047
+ celestial_point_language=self._language_model.celestial_points,
1727
2048
  )
1728
2049
  template_dict["makeSecondaryPlanetGrid"] = draw_secondary_planet_grid(
1729
2050
  planets_and_houses_grid_title="",
@@ -1731,7 +2052,7 @@ class ChartDrawer:
1731
2052
  second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
1732
2053
  chart_type=self.chart_type,
1733
2054
  text_color=self.chart_colors_settings["paper_0"],
1734
- celestial_point_language=self.language_settings["celestial_points"],
2055
+ celestial_point_language=self._language_model.celestial_points,
1735
2056
  )
1736
2057
 
1737
2058
  house_comparison_factory = HouseComparisonFactory(
@@ -1743,13 +2064,13 @@ class ChartDrawer:
1743
2064
 
1744
2065
  template_dict["makeHouseComparisonGrid"] = draw_house_comparison_grid(
1745
2066
  house_comparison,
1746
- celestial_point_language=self.language_settings["celestial_points"],
2067
+ celestial_point_language=self._language_model.celestial_points,
1747
2068
  active_points=self.active_points,
1748
2069
  points_owner_subject_number=2, # The second subject is the Solar Return
1749
- house_position_comparison_label=self.language_settings.get("house_position_comparison", "House Position Comparison"),
1750
- return_point_label=self.language_settings.get("return_point", "Return Point"),
1751
- return_label=self.language_settings.get("Return", "DualReturnChart"),
1752
- 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"),
1753
2074
  )
1754
2075
 
1755
2076
  elif self.chart_type == "SingleReturnChart":
@@ -1800,40 +2121,40 @@ class ChartDrawer:
1800
2121
  template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
1801
2122
 
1802
2123
  # Top left section
1803
- latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
1804
- 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"))
1805
2126
 
1806
- template_dict["top_left_0"] = f'{self.language_settings["info"]}:'
2127
+ template_dict["top_left_0"] = f'{self._translate("info", "Info")}:'
1807
2128
  template_dict["top_left_1"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
1808
2129
  template_dict["top_left_2"] = f"{self.first_obj.city}, {self.first_obj.nation}"
1809
- template_dict["top_left_3"] = f"{self.language_settings['latitude']}: {latitude_string}"
1810
- 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}"
1811
2132
 
1812
2133
  if hasattr(self.first_obj, 'return_type') and self.first_obj.return_type == "Solar":
1813
- 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')}"
1814
2135
  else:
1815
- 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')}"
1816
2137
 
1817
2138
  # Bottom left section
1818
2139
  if self.first_obj.zodiac_type == "Tropic":
1819
- 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')}"
1820
2141
  else:
1821
2142
  mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
1822
2143
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
1823
- zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
2144
+ zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
1824
2145
 
1825
2146
  template_dict["bottom_left_0"] = zodiac_info
1826
- 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')}"
1827
2148
 
1828
2149
  # Lunar phase information (optional)
1829
2150
  if self.first_obj.lunar_phase is not None:
1830
- template_dict["bottom_left_2"] = f'{self.language_settings.get("lunation_day", "Lunation Day")}: {self.first_obj.lunar_phase.get("moon_phase", "")}'
1831
- 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)}'
1832
2153
  else:
1833
2154
  template_dict["bottom_left_2"] = ""
1834
2155
  template_dict["bottom_left_3"] = ""
1835
2156
 
1836
- 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)}'
1837
2158
 
1838
2159
  # Moon phase section calculations
1839
2160
  if self.first_obj.lunar_phase is not None:
@@ -1845,7 +2166,7 @@ class ChartDrawer:
1845
2166
  template_dict["makeMainHousesGrid"] = draw_main_house_grid(
1846
2167
  main_subject_houses_list=first_subject_houses_list,
1847
2168
  text_color=self.chart_colors_settings["paper_0"],
1848
- house_cusp_generale_name_label=self.language_settings["cusp"],
2169
+ house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
1849
2170
  )
1850
2171
  template_dict["makeSecondaryHousesGrid"] = ""
1851
2172
 
@@ -1875,12 +2196,12 @@ class ChartDrawer:
1875
2196
  )
1876
2197
 
1877
2198
  template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
1878
- planets_and_houses_grid_title=self.language_settings["planets_and_house"],
2199
+ planets_and_houses_grid_title=self._translate("planets_and_house", "Points for"),
1879
2200
  subject_name=self.first_obj.name,
1880
2201
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
1881
2202
  chart_type=self.chart_type,
1882
2203
  text_color=self.chart_colors_settings["paper_0"],
1883
- celestial_point_language=self.language_settings["celestial_points"],
2204
+ celestial_point_language=self._language_model.celestial_points,
1884
2205
  )
1885
2206
  template_dict["makeSecondaryPlanetGrid"] = ""
1886
2207
  template_dict["makeHouseComparisonGrid"] = ""