Skip to content

Commit 8dd6bde

Browse files
committed
feat(translations): enhance overview with summary statistics and improve attribute coverage display
1 parent 5ccb8a4 commit 8dd6bde

File tree

2 files changed

+198
-82
lines changed

2 files changed

+198
-82
lines changed

app/controllers/better_together/translations_controller.rb

Lines changed: 126 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -113,20 +113,44 @@ def collect_available_attributes(model_filter = 'all')
113113
model_class = model_filter.constantize
114114
attributes = []
115115

116-
# Add mobility attributes
116+
# Add mobility attributes from the model itself
117117
if model_class.respond_to?(:mobility_attributes)
118118
model_class.mobility_attributes.each do |attr|
119119
attributes << { name: attr.to_s, type: 'text', source: 'mobility' }
120120
end
121121
end
122122

123-
# Add translatable attachment attributes
123+
# Add translatable attachment attributes from the model itself
124124
if model_class.respond_to?(:mobility_translated_attachments)
125125
model_class.mobility_translated_attachments&.keys&.each do |attr|
126126
attributes << { name: attr.to_s, type: 'file', source: 'attachment' }
127127
end
128128
end
129129

130+
# For STI models, also check descendants for translatable attributes
131+
load_subclasses(model_class)
132+
if model_class.respond_to?(:descendants) && model_class.descendants.any?
133+
model_class.descendants.each do |subclass|
134+
# Add mobility attributes from subclass
135+
if subclass.respond_to?(:mobility_attributes)
136+
subclass.mobility_attributes.each do |attr|
137+
attributes << { name: attr.to_s, type: 'text', source: 'mobility' } unless attributes.any? do |a|
138+
a[:name] == attr.to_s
139+
end
140+
end
141+
end
142+
143+
# Add translatable attachment attributes from subclass
144+
next unless subclass.respond_to?(:mobility_translated_attachments)
145+
146+
subclass.mobility_translated_attachments&.keys&.each do |attr|
147+
attributes << { name: attr.to_s, type: 'file', source: 'attachment' } unless attributes.any? do |a|
148+
a[:name] == attr.to_s
149+
end
150+
end
151+
end
152+
end
153+
130154
attributes.sort_by { |attr| attr[:name] }
131155
rescue StandardError => e
132156
Rails.logger.error "Error collecting attributes for #{model_filter}: #{e.message}"
@@ -626,8 +650,16 @@ def calculate_model_instance_stats
626650
# Get attribute-specific coverage
627651
attribute_coverage = calculate_attribute_coverage_for_model(model_name, model_class)
628652

629-
# Calculate coverage percentage with bounds checking
630-
coverage_percentage = if total_instances.positive? && translated_instances <= total_instances
653+
# Calculate overall coverage percentage as average of attribute coverages
654+
# This is more accurate than just counting instances with ANY translation
655+
coverage_percentage = if attribute_coverage&.any?
656+
# Calculate average coverage across all attributes
657+
attribute_percentages = attribute_coverage.values.map do |attr|
658+
attr[:coverage_percentage] || 0.0
659+
end
660+
(attribute_percentages.sum / attribute_percentages.size).round(1)
661+
elsif total_instances.positive? && translated_instances <= total_instances
662+
# Fallback to instance-based calculation if no attributes
631663
(translated_instances.to_f / total_instances * 100).round(1)
632664
elsif translated_instances > total_instances
633665
Rails.logger.warn "Translation coverage anomaly for #{model_name}: #{translated_instances} translated > #{total_instances} total"
@@ -725,33 +757,59 @@ def calculate_attribute_coverage_for_model(model_name, model_class)
725757
model_class.count
726758
end
727759

728-
# Get all mobility attributes for this model
760+
# Debug logging for troubleshooting
761+
Rails.logger.debug "Calculating coverage for #{model_name}: #{total_instances} total instances"
762+
Rails.logger.debug "Has mobility_attributes? #{model_class.respond_to?(:mobility_attributes)}"
729763
if model_class.respond_to?(:mobility_attributes)
730-
model_class.mobility_attributes.each do |attribute|
731-
attribute_name = attribute.to_s
764+
Rails.logger.debug "Mobility attributes: #{model_class.mobility_attributes.inspect}"
765+
end
732766

733-
# Count instances with translations for this specific attribute
734-
instances_with_attribute = count_instances_with_attribute_translations(model_name, attribute_name)
767+
# Collect mobility attributes from the model and its subclasses (for STI)
768+
all_attributes = Set.new
735769

736-
# Calculate coverage with bounds checking
737-
coverage_percentage = if total_instances.positive? && instances_with_attribute <= total_instances
738-
(instances_with_attribute.to_f / total_instances * 100).round(1)
739-
elsif instances_with_attribute > total_instances
740-
Rails.logger.warn "Attribute coverage anomaly for #{model_name}.#{attribute_name}: #{instances_with_attribute} > #{total_instances}"
741-
100.0
742-
else
743-
0.0
744-
end
770+
# Load subclasses to ensure they're available in development
771+
load_subclasses(model_class)
745772

746-
coverage[attribute_name] = {
747-
instances_translated: instances_with_attribute,
748-
total_instances: total_instances,
749-
coverage_percentage: coverage_percentage,
750-
attribute_type: 'mobility'
751-
}
773+
# Get attributes from the main model
774+
if model_class.respond_to?(:mobility_attributes)
775+
model_class.mobility_attributes.each { |attr| all_attributes.add(attr.to_s) }
776+
end
777+
778+
# For STI models, also check subclasses for their translatable attributes
779+
if model_class.respond_to?(:descendants) && model_class.descendants.any?
780+
model_class.descendants.each do |subclass|
781+
if subclass.respond_to?(:mobility_attributes)
782+
Rails.logger.debug "STI subclass #{subclass.name} has attributes: #{subclass.mobility_attributes.inspect}"
783+
subclass.mobility_attributes.each { |attr| all_attributes.add(attr.to_s) }
784+
end
752785
end
753786
end
754787

788+
Rails.logger.debug "All collected attributes for #{model_name}: #{all_attributes.to_a.inspect}"
789+
790+
# Calculate coverage for each unique attribute
791+
all_attributes.each do |attribute_name|
792+
# Count instances with translations for this specific attribute
793+
instances_with_attribute = count_instances_with_attribute_translations(model_name, attribute_name)
794+
795+
# Calculate coverage with bounds checking
796+
coverage_percentage = if total_instances.positive? && instances_with_attribute <= total_instances
797+
(instances_with_attribute.to_f / total_instances * 100).round(1)
798+
elsif instances_with_attribute > total_instances
799+
Rails.logger.warn "Attribute coverage anomaly for #{model_name}.#{attribute_name}: #{instances_with_attribute} > #{total_instances}"
800+
100.0
801+
else
802+
0.0
803+
end
804+
805+
coverage[attribute_name] = {
806+
instances_translated: instances_with_attribute,
807+
total_instances: total_instances,
808+
coverage_percentage: coverage_percentage,
809+
attribute_type: 'mobility'
810+
}
811+
end
812+
755813
# Get translatable attachment attributes
756814
if model_class.respond_to?(:mobility_translated_attachments)
757815
model_class.mobility_translated_attachments&.keys&.each do |attachment_name|
@@ -1091,5 +1149,49 @@ def truncate_value(value, limit = 100)
10911149
text = value.to_s.strip
10921150
text.length > limit ? "#{text[0..limit]}..." : text
10931151
end
1152+
1153+
# Load subclasses for STI models to ensure they're available in development environment
1154+
def load_subclasses(model_class)
1155+
return unless model_class.respond_to?(:descendants)
1156+
1157+
# In development, Rails lazy-loads classes, so we need to force-load STI subclasses
1158+
if Rails.env.development?
1159+
# Get the base model's directory path
1160+
base_path = Rails.application.root.join('app', 'models')
1161+
engine_path = BetterTogether::Engine.root.join('app', 'models')
1162+
1163+
# Convert class name to file path pattern
1164+
model_path = model_class.name.underscore
1165+
1166+
# Look for subclass files in both app and engine models
1167+
[base_path, engine_path].each do |path|
1168+
# Check for files in the same directory as the base model
1169+
model_dir = File.dirname(model_path)
1170+
pattern = path.join("#{model_dir}/*.rb")
1171+
1172+
Dir.glob(pattern).each do |file|
1173+
# Extract class name from file path and try to constantize it
1174+
relative_path = Pathname.new(file).relative_path_from(path).to_s
1175+
class_name = relative_path.gsub('.rb', '').camelize
1176+
1177+
begin
1178+
# Only try to load if it's not the same as the base class
1179+
next if class_name == model_class.name
1180+
1181+
loaded_class = class_name.constantize
1182+
1183+
# Check if it's actually a subclass of our model
1184+
if loaded_class.ancestors.include?(model_class) && loaded_class != model_class
1185+
Rails.logger.debug "Successfully loaded subclass: #{class_name}"
1186+
end
1187+
rescue NameError, LoadError => e
1188+
Rails.logger.debug "Could not load potential subclass #{class_name}: #{e.message}"
1189+
end
1190+
end
1191+
end
1192+
end
1193+
rescue StandardError => e
1194+
Rails.logger.warn "Error loading subclasses for #{model_class.name}: #{e.message}"
1195+
end
10941196
end
10951197
end

app/views/better_together/translations/_overview.html.erb

Lines changed: 72 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,66 @@
1-
<!-- Data Type Overview -->
2-
<div class="row mb-4">
1+
2+
<!-- Summary Statistics -->
3+
<div class="row">
34
<div class="col-12">
5+
<div class="card">
6+
<div class="card-header">
7+
<h5 class="card-title mb-0">
8+
<i class="fa-solid fa-calculator me-2" aria-hidden="true"></i>
9+
<%= t('.summary_statistics') %>
10+
</h5>
11+
</div>
12+
<div class="card-body">
13+
<div class="row text-center">
14+
<div class="col-md-3 mb-3">
15+
<div class="border rounded p-3">
16+
<h3 class="text-primary mb-0">
17+
<%= number_with_delimiter(@total_translation_records || 0) %>
18+
</h3>
19+
<small class="text-muted"><%= t('.total_translation_records') %></small>
20+
</div>
21+
</div>
22+
<div class="col-md-3 mb-3">
23+
<div class="border rounded p-3">
24+
<h3 class="text-success mb-0">
25+
<%= @available_locales&.size || 0 %>
26+
</h3>
27+
<small class="text-muted"><%= t('.supported_locales') %></small>
28+
</div>
29+
</div>
30+
<div class="col-md-3 mb-3">
31+
<div class="border rounded p-3">
32+
<h3 class="text-info mb-0">
33+
<%= @available_model_types&.size || 0 %>
34+
</h3>
35+
<small class="text-muted"><%= t('.translatable_models') %></small>
36+
</div>
37+
</div>
38+
<div class="col-md-3 mb-3">
39+
<div class="border rounded p-3">
40+
<h3 class="text-warning mb-0">
41+
<%= @available_attributes&.size || 0 %>
42+
</h3>
43+
<small class="text-muted"><%= t('.unique_attributes') %></small>
44+
</div>
45+
</div>
46+
</div>
47+
</div>
48+
</div>
49+
</div>
50+
</div>
51+
52+
<div class="row mt-3">
53+
<div class="col">
454
<h2 class="h4 mb-3">
555
<i class="fa-solid fa-database me-2" aria-hidden="true"></i>
656
<%= t('.data_type_overview') %>
757
</h2>
58+
</div>
59+
</div>
60+
61+
<!-- Data Type Overview -->
62+
<div class="row mb-4">
63+
<div class="col-12">
864
<div class="row">
965
<% @data_type_summary.each do |type, info| %>
1066
<div class="col-lg-3 col-md-6 mb-3">
@@ -158,8 +214,8 @@
158214
<h6 class="card-title mb-0">
159215
<%= model_name.split('::').last %>
160216
</h6>
161-
<span class="badge bg-<%= (stats[:coverage_percentage] || 0) >= 75 ? 'success' : (stats[:coverage_percentage] || 0) >= 50 ? 'warning' : 'danger' %>">
162-
<%= stats[:coverage_percentage] || 0 %>% coverage
217+
<span class="badge bg-<%= (stats[:translation_coverage] || 0) >= 75 ? 'success' : (stats[:translation_coverage] || 0) >= 50 ? 'warning' : 'danger' %>">
218+
<%= stats[:translation_coverage] || 0 %>% coverage
163219
</span>
164220
</div>
165221
<div class="card-body">
@@ -179,10 +235,10 @@
179235

180236
<!-- Progress Bar -->
181237
<div class="progress mb-3" style="height: 8px;">
182-
<div class="progress-bar bg-<%= (stats[:coverage_percentage] || 0) >= 75 ? 'success' : (stats[:coverage_percentage] || 0) >= 50 ? 'warning' : 'danger' %>"
238+
<div class="progress-bar bg-<%= (stats[:translation_coverage] || 0) >= 75 ? 'success' : (stats[:translation_coverage] || 0) >= 50 ? 'warning' : 'danger' %>"
183239
role="progressbar"
184-
style="width: <%= stats[:coverage_percentage] || 0 %>%"
185-
aria-valuenow="<%= stats[:coverage_percentage] || 0 %>"
240+
style="width: <%= stats[:translation_coverage] || 0 %>%"
241+
aria-valuenow="<%= stats[:translation_coverage] || 0 %>"
186242
aria-valuemin="0"
187243
aria-valuemax="100">
188244
</div>
@@ -211,8 +267,16 @@
211267
</div>
212268
<% end %>
213269
</div>
270+
<% elsif stats[:total_instances] == 0 %>
271+
<p class="small text-muted text-center">
272+
<i class="fa-solid fa-database me-1"></i>
273+
<%= t('.no_instances_found') %>
274+
</p>
214275
<% else %>
215-
<p class="small text-muted"><%= t('.no_attribute_coverage') %></p>
276+
<p class="small text-muted text-center">
277+
<i class="fa-solid fa-language me-1"></i>
278+
<%= t('.no_translatable_attributes') %>
279+
</p>
216280
<% end %>
217281
</div>
218282
</div>
@@ -227,53 +291,3 @@
227291
<% end %>
228292
</div>
229293
</div>
230-
231-
<!-- Summary Statistics -->
232-
<div class="row">
233-
<div class="col-12">
234-
<div class="card">
235-
<div class="card-header">
236-
<h5 class="card-title mb-0">
237-
<i class="fa-solid fa-calculator me-2" aria-hidden="true"></i>
238-
<%= t('.summary_statistics') %>
239-
</h5>
240-
</div>
241-
<div class="card-body">
242-
<div class="row text-center">
243-
<div class="col-md-3 mb-3">
244-
<div class="border rounded p-3">
245-
<h3 class="text-primary mb-0">
246-
<%= number_with_delimiter(@total_translation_records || 0) %>
247-
</h3>
248-
<small class="text-muted"><%= t('.total_translation_records') %></small>
249-
</div>
250-
</div>
251-
<div class="col-md-3 mb-3">
252-
<div class="border rounded p-3">
253-
<h3 class="text-success mb-0">
254-
<%= @available_locales&.size || 0 %>
255-
</h3>
256-
<small class="text-muted"><%= t('.supported_locales') %></small>
257-
</div>
258-
</div>
259-
<div class="col-md-3 mb-3">
260-
<div class="border rounded p-3">
261-
<h3 class="text-info mb-0">
262-
<%= @available_model_types&.size || 0 %>
263-
</h3>
264-
<small class="text-muted"><%= t('.translatable_models') %></small>
265-
</div>
266-
</div>
267-
<div class="col-md-3 mb-3">
268-
<div class="border rounded p-3">
269-
<h3 class="text-warning mb-0">
270-
<%= @available_attributes&.size || 0 %>
271-
</h3>
272-
<small class="text-muted"><%= t('.unique_attributes') %></small>
273-
</div>
274-
</div>
275-
</div>
276-
</div>
277-
</div>
278-
</div>
279-
</div>

0 commit comments

Comments
 (0)