Skip to content

Commit c23cb27

Browse files
committed
feat(translations): add unique translated records statistics and improve overview layout
1 parent 8dd6bde commit c23cb27

File tree

5 files changed

+187
-27
lines changed

5 files changed

+187
-27
lines changed

app/controllers/better_together/translations_controller.rb

Lines changed: 167 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def index
66
# For overview tab - prepare statistics data
77
@available_locales = I18n.available_locales.map(&:to_s)
88
@available_model_types = collect_all_model_types
9-
@available_attributes = collect_available_attributes('all')
9+
@available_attributes = collect_all_attributes
1010

1111
@data_type_summary = build_data_type_summary
1212
@data_type_stats = calculate_data_type_stats
@@ -16,6 +16,7 @@ def index
1616
@model_type_stats = calculate_model_type_stats
1717
@attribute_stats = calculate_attribute_stats
1818
@total_translation_records = calculate_total_records
19+
@unique_translated_records = calculate_unique_translated_records
1920

2021
# Calculate model instance translation coverage
2122
@model_instance_stats = calculate_model_instance_stats
@@ -98,9 +99,22 @@ def collect_all_model_types
9899
collect_rich_text_translation_models(model_types)
99100
collect_file_translation_models(model_types)
100101

101-
# Convert to array and constantize
102+
# Convert to array, constantize, and filter for models with actual translatable attributes
102103
model_types.map do |type_name|
103-
{ name: type_name, class: type_name.constantize }
104+
model_class = type_name.constantize
105+
106+
# Load subclasses to ensure STI descendants are available
107+
load_subclasses(model_class)
108+
109+
# Check if the model actually has translatable attributes
110+
has_translatable_attributes = has_translatable_attributes?(model_class)
111+
112+
if has_translatable_attributes
113+
{ name: type_name, class: model_class }
114+
else
115+
Rails.logger.debug "Skipping #{type_name}: no translatable attributes found"
116+
nil
117+
end
104118
rescue StandardError => e
105119
Rails.logger.warn "Could not constantize model type #{type_name}: #{e.message}"
106120
nil
@@ -338,20 +352,55 @@ def collect_text_translation_models(model_types)
338352
def collect_rich_text_translation_models(model_types)
339353
return unless defined?(ActionText::RichText)
340354

355+
# Get unique combinations of record_type and name to validate translatable attributes
341356
ActionText::RichText
342357
.distinct
343-
.pluck(:record_type)
344-
.each { |type| model_types.add(type) }
358+
.pluck(:record_type, :name)
359+
.each do |record_type, attribute_name|
360+
next unless record_type.present? && attribute_name.present?
361+
362+
begin
363+
model_class = record_type.constantize
364+
load_subclasses(model_class)
365+
366+
# Check if this specific attribute is translatable in the model or its descendants
367+
if has_translatable_rich_text_attribute?(model_class, attribute_name)
368+
model_types.add(record_type)
369+
else
370+
Rails.logger.debug "Skipping #{record_type}: attribute '#{attribute_name}' not found in translatable rich text attributes"
371+
end
372+
rescue StandardError => e
373+
Rails.logger.warn "Could not check rich text translatability for #{record_type}: #{e.message}"
374+
end
375+
end
345376
end
346377

347378
def collect_file_translation_models(model_types)
348379
return unless defined?(ActiveStorage::Attachment) &&
349380
ActiveStorage::Attachment.column_names.include?('locale')
350381

382+
# Get unique combinations of record_type and name to validate translatable attachments
351383
ActiveStorage::Attachment
384+
.where.not(locale: [nil, '']) # Only include records with actual locale values
352385
.distinct
353-
.pluck(:record_type)
354-
.each { |type| model_types.add(type) }
386+
.pluck(:record_type, :name)
387+
.each do |record_type, attachment_name|
388+
next unless record_type.present? && attachment_name.present?
389+
390+
begin
391+
model_class = record_type.constantize
392+
load_subclasses(model_class)
393+
394+
# Check if this specific attachment is translatable in the model or its descendants
395+
if has_translatable_attachment?(model_class, attachment_name)
396+
model_types.add(record_type)
397+
else
398+
Rails.logger.debug "Skipping #{record_type}: attachment '#{attachment_name}' not found in translatable attachments"
399+
end
400+
rescue StandardError => e
401+
Rails.logger.warn "Could not check file translatability for #{record_type}: #{e.message}"
402+
end
403+
end
355404
end
356405

357406
def group_models_by_namespace(models)
@@ -624,6 +673,45 @@ def calculate_total_records
624673
count
625674
end
626675

676+
def calculate_unique_translated_records
677+
unique_records = Set.new
678+
679+
# Collect unique (model_type, record_id) pairs from string translations
680+
if defined?(Mobility::Backends::ActiveRecord::KeyValue::StringTranslation)
681+
Mobility::Backends::ActiveRecord::KeyValue::StringTranslation
682+
.distinct
683+
.pluck(:translatable_type, :translatable_id)
684+
.each { |type, id| unique_records.add([type, id]) }
685+
end
686+
687+
# Collect from text translations
688+
if defined?(Mobility::Backends::ActiveRecord::KeyValue::TextTranslation)
689+
Mobility::Backends::ActiveRecord::KeyValue::TextTranslation
690+
.distinct
691+
.pluck(:translatable_type, :translatable_id)
692+
.each { |type, id| unique_records.add([type, id]) }
693+
end
694+
695+
# Collect from rich text translations
696+
if defined?(ActionText::RichText)
697+
ActionText::RichText
698+
.distinct
699+
.pluck(:record_type, :record_id)
700+
.each { |type, id| unique_records.add([type, id]) }
701+
end
702+
703+
# Collect from file translations
704+
if defined?(ActiveStorage::Attachment) && ActiveStorage::Attachment.column_names.include?('locale')
705+
ActiveStorage::Attachment
706+
.where.not(locale: [nil, ''])
707+
.distinct
708+
.pluck(:record_type, :record_id)
709+
.each { |type, id| unique_records.add([type, id]) }
710+
end
711+
712+
unique_records.size
713+
end
714+
627715
# Calculate unique model instance translation coverage
628716
def calculate_model_instance_stats
629717
stats = {}
@@ -1098,6 +1186,15 @@ def collect_all_attributes
10981186
.each { |attr| attributes.add(attr) }
10991187
end
11001188

1189+
# Collect from file translations (ActiveStorage attachments with locale)
1190+
if defined?(ActiveStorage::Attachment) && ActiveStorage::Attachment.column_names.include?('locale')
1191+
ActiveStorage::Attachment
1192+
.where.not(locale: [nil, ''])
1193+
.distinct
1194+
.pluck(:name)
1195+
.each { |attr| attributes.add(attr) }
1196+
end
1197+
11011198
attributes.to_a.sort
11021199
end
11031200

@@ -1193,5 +1290,68 @@ def load_subclasses(model_class)
11931290
rescue StandardError => e
11941291
Rails.logger.warn "Error loading subclasses for #{model_class.name}: #{e.message}"
11951292
end
1293+
1294+
# Check if a model class has any translatable attributes (including STI descendants)
1295+
def has_translatable_attributes?(model_class)
1296+
# Check mobility attributes on the model itself
1297+
return true if model_class.respond_to?(:mobility_attributes) && model_class.mobility_attributes.any?
1298+
1299+
# Check translatable attachments on the model itself
1300+
if model_class.respond_to?(:mobility_translated_attachments) && model_class.mobility_translated_attachments&.any?
1301+
return true
1302+
end
1303+
1304+
# For STI models, check descendants
1305+
if model_class.respond_to?(:descendants) && model_class.descendants.any?
1306+
model_class.descendants.each do |subclass|
1307+
return true if subclass.respond_to?(:mobility_attributes) && subclass.mobility_attributes.any?
1308+
if subclass.respond_to?(:mobility_translated_attachments) && subclass.mobility_translated_attachments&.any?
1309+
return true
1310+
end
1311+
end
1312+
end
1313+
1314+
false
1315+
end
1316+
1317+
# Check if a model has a specific translatable rich text attribute
1318+
def has_translatable_rich_text_attribute?(model_class, attribute_name)
1319+
# Check if the model has this attribute configured for Action Text translation
1320+
if model_class.respond_to?(:mobility_attributes)
1321+
mobility_configs = model_class.mobility.attributes_hash
1322+
return true if mobility_configs[attribute_name.to_sym]&.dig(:backend) == :action_text
1323+
end
1324+
1325+
# Check STI descendants
1326+
if model_class.respond_to?(:descendants) && model_class.descendants.any?
1327+
model_class.descendants.each do |subclass|
1328+
next unless subclass.respond_to?(:mobility_attributes)
1329+
1330+
mobility_configs = subclass.mobility.attributes_hash
1331+
return true if mobility_configs[attribute_name.to_sym]&.dig(:backend) == :action_text
1332+
end
1333+
end
1334+
1335+
false
1336+
end
1337+
1338+
# Check if a model has a specific translatable attachment
1339+
def has_translatable_attachment?(model_class, attachment_name)
1340+
# Check if the model has this attachment configured as translatable
1341+
if model_class.respond_to?(:mobility_translated_attachments)
1342+
return model_class.mobility_translated_attachments&.key?(attachment_name.to_sym)
1343+
end
1344+
1345+
# Check STI descendants
1346+
if model_class.respond_to?(:descendants) && model_class.descendants.any?
1347+
model_class.descendants.each do |subclass|
1348+
if subclass.respond_to?(:mobility_translated_attachments) && subclass.mobility_translated_attachments&.key?(attachment_name.to_sym)
1349+
return true
1350+
end
1351+
end
1352+
end
1353+
1354+
false
1355+
end
11961356
end
11971357
end

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

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,39 @@
1111
</div>
1212
<div class="card-body">
1313
<div class="row text-center">
14-
<div class="col-md-3 mb-3">
14+
<div class="col-lg-2 col-md-4 col-sm-6 mb-3">
1515
<div class="border rounded p-3">
1616
<h3 class="text-primary mb-0">
1717
<%= number_with_delimiter(@total_translation_records || 0) %>
1818
</h3>
1919
<small class="text-muted"><%= t('.total_translation_records') %></small>
2020
</div>
2121
</div>
22-
<div class="col-md-3 mb-3">
22+
<div class="col-lg-2 col-md-4 col-sm-6 mb-3">
23+
<div class="border rounded p-3">
24+
<h3 class="text-secondary mb-0">
25+
<%= number_with_delimiter(@unique_translated_records || 0) %>
26+
</h3>
27+
<small class="text-muted"><%= t('.unique_translated_records') %></small>
28+
</div>
29+
</div>
30+
<div class="col-lg-2 col-md-4 col-sm-6 mb-3">
2331
<div class="border rounded p-3">
2432
<h3 class="text-success mb-0">
2533
<%= @available_locales&.size || 0 %>
2634
</h3>
2735
<small class="text-muted"><%= t('.supported_locales') %></small>
2836
</div>
2937
</div>
30-
<div class="col-md-3 mb-3">
38+
<div class="col-lg-2 col-md-4 col-sm-6 mb-3">
3139
<div class="border rounded p-3">
3240
<h3 class="text-info mb-0">
3341
<%= @available_model_types&.size || 0 %>
3442
</h3>
3543
<small class="text-muted"><%= t('.translatable_models') %></small>
3644
</div>
3745
</div>
38-
<div class="col-md-3 mb-3">
46+
<div class="col-lg-2 col-md-4 col-sm-6 mb-3">
3947
<div class="border rounded p-3">
4048
<h3 class="text-warning mb-0">
4149
<%= @available_attributes&.size || 0 %>
@@ -173,7 +181,7 @@
173181
</div>
174182
<div class="card-body">
175183
<% if @attribute_stats&.any? %>
176-
<% @attribute_stats.first(10).each do |attribute, count| %>
184+
<% @attribute_stats.each do |attribute, count| %>
177185
<div class="d-flex justify-content-between align-items-center mb-2">
178186
<span class="badge bg-warning text-dark">
179187
<%= attribute %>
@@ -183,13 +191,6 @@
183191
</span>
184192
</div>
185193
<% end %>
186-
<% if @attribute_stats.size > 10 %>
187-
<div class="text-center mt-3">
188-
<small class="text-muted">
189-
<%= t('.and_more_attributes', count: @attribute_stats.size - 10) %>
190-
</small>
191-
</div>
192-
<% end %>
193194
<% else %>
194195
<p class="text-muted"><%= t('.no_attribute_data') %></p>
195196
<% end %>
@@ -248,7 +249,7 @@
248249
<% if stats[:attribute_coverage]&.any? %>
249250
<h6 class="small text-muted mb-2"><%= t('.attribute_coverage_title') %></h6>
250251
<div class="attribute-coverage">
251-
<% stats[:attribute_coverage].first(5).each do |attr, attr_stats| %>
252+
<% stats[:attribute_coverage].each do |attr, attr_stats| %>
252253
<div class="d-flex justify-content-between align-items-center mb-1">
253254
<span class="badge text-bg-<%= attr_stats[:attribute_type] == 'file' ? 'info' : 'secondary' %> small">
254255
<%= attr %>
@@ -259,13 +260,6 @@
259260
</small>
260261
</div>
261262
<% end %>
262-
<% if stats[:attribute_coverage].size > 5 %>
263-
<div class="text-center mt-2">
264-
<small class="text-muted">
265-
<%= t('.and_more_attributes', count: stats[:attribute_coverage].size - 5) %>
266-
</small>
267-
</div>
268-
<% end %>
269263
</div>
270264
<% elsif stats[:total_instances] == 0 %>
271265
<p class="small text-muted text-center">

config/locales/en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,6 +1515,7 @@ en:
15151515
no_attribute_data: No attribute data available
15161516
and_more_attributes: "and %{count} more attributes..."
15171517
total_translation_records: Total Translation Records
1518+
unique_translated_records: Unique Translated Records
15181519
supported_locales: Supported Locales
15191520
translatable_models: Translatable Models
15201521
unique_attributes: Unique Attributes
@@ -1568,6 +1569,7 @@ en:
15681569
no_attribute_data: No attribute data available
15691570
and_more_attributes: "and %{count} more attributes..."
15701571
total_translation_records: Total Translation Records
1572+
unique_translated_records: Unique Translated Records
15711573
supported_locales: Supported Locales
15721574
translatable_models: Translatable Models
15731575
unique_attributes: Unique Attributes

config/locales/es.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,6 +1526,7 @@ es:
15261526
no_attribute_data: No hay datos de atributo disponibles
15271527
and_more_attributes: "y %{count} atributos más..."
15281528
total_translation_records: Total de Registros de Traducción
1529+
unique_translated_records: Registros Traducidos Únicos
15291530
supported_locales: Idiomas Soportados
15301531
translatable_models: Modelos Traducibles
15311532
unique_attributes: Atributos Únicos
@@ -1579,6 +1580,7 @@ es:
15791580
no_attribute_data: No hay datos de atributo disponibles
15801581
and_more_attributes: "y %{count} atributos más..."
15811582
total_translation_records: Total de Registros de Traducción
1583+
unique_translated_records: Registros Traducidos Únicos
15821584
supported_locales: Idiomas Soportados
15831585
translatable_models: Modelos Traducibles
15841586
unique_attributes: Atributos Únicos

config/locales/fr.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,7 @@ fr:
15341534
no_attribute_data: Aucune donnée d'attribut disponible
15351535
and_more_attributes: "et %{count} attributs de plus..."
15361536
total_translation_records: Total des Enregistrements de Traduction
1537+
unique_translated_records: Enregistrements Traduits Uniques
15371538
supported_locales: Langues Supportées
15381539
translatable_models: Modèles Traduisibles
15391540
unique_attributes: Attributs Uniques
@@ -1587,6 +1588,7 @@ fr:
15871588
no_attribute_data: Aucune donnée d'attribut disponible
15881589
and_more_attributes: "et %{count} attributs de plus..."
15891590
total_translation_records: Total des Enregistrements de Traduction
1591+
unique_translated_records: Enregistrements Traduits Uniques
15901592
supported_locales: Langues Supportées
15911593
translatable_models: Modèles Traduisibles
15921594
unique_attributes: Attributs Uniques

0 commit comments

Comments
 (0)