Skip to content

Commit a2ba1cb

Browse files
committed
Add locale-specific and overall translation coverage views
- Updated the translations overview to include a toggle for standard and locale-focused views. - Introduced new partials for locale-specific coverage and overall coverage analysis. - Enhanced the UI with progress bars and detailed statistics for translation coverage by locale. - Modified existing translation messages to accommodate new features and improved clarity. - Added JavaScript for dynamic view toggling and locale detail display.
1 parent c23cb27 commit a2ba1cb

File tree

9 files changed

+1301
-62
lines changed

9 files changed

+1301
-62
lines changed

app/controllers/better_together/translations_controller.rb

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ def index
2020

2121
# Calculate model instance translation coverage
2222
@model_instance_stats = calculate_model_instance_stats
23+
24+
# Calculate locale gap summary for enhanced view
25+
@locale_gap_summary = calculate_locale_gap_summary
2326
end
2427

2528
def by_locale
@@ -1353,5 +1356,285 @@ def has_translatable_attachment?(model_class, attachment_name)
13531356

13541357
false
13551358
end
1359+
1360+
# Calculate per-locale translation coverage for a specific model
1361+
def calculate_locale_coverage_for_model(_model_name, model_class)
1362+
locale_coverage = {}
1363+
1364+
# Get all translatable attributes for this model (including STI descendants)
1365+
all_attributes = collect_model_translatable_attributes(model_class)
1366+
return locale_coverage if all_attributes.empty?
1367+
1368+
# Calculate coverage for each available locale
1369+
I18n.available_locales.each do |locale|
1370+
locale_str = locale.to_s
1371+
locale_coverage[locale_str] = {
1372+
total_attributes: all_attributes.length,
1373+
translated_attributes: 0,
1374+
missing_attributes: [],
1375+
completion_percentage: 0.0
1376+
}
1377+
1378+
all_attributes.each do |attribute_name, backend_type|
1379+
has_translation = case backend_type
1380+
when :string, :text
1381+
has_string_text_translation?(model_class, attribute_name, locale_str)
1382+
when :action_text
1383+
has_action_text_translation?(model_class, attribute_name, locale_str)
1384+
when :active_storage
1385+
has_active_storage_translation?(model_class, attribute_name, locale_str)
1386+
else
1387+
false
1388+
end
1389+
1390+
if has_translation
1391+
locale_coverage[locale_str][:translated_attributes] += 1
1392+
else
1393+
locale_coverage[locale_str][:missing_attributes] << attribute_name
1394+
end
1395+
end
1396+
1397+
# Calculate completion percentage
1398+
next unless locale_coverage[locale_str][:total_attributes] > 0
1399+
1400+
locale_coverage[locale_str][:completion_percentage] =
1401+
(locale_coverage[locale_str][:translated_attributes].to_f /
1402+
locale_coverage[locale_str][:total_attributes] * 100).round(1)
1403+
end
1404+
1405+
locale_coverage
1406+
end
1407+
1408+
# Check if model has translation for specific string/text attribute in given locale
1409+
def has_string_text_translation?(model_class, attribute_name, locale)
1410+
# Use KeyValue backend - check mobility_string_translations and mobility_text_translations
1411+
string_table = Mobility::Backends::ActiveRecord::KeyValue::StringTranslation.table_name
1412+
text_table = Mobility::Backends::ActiveRecord::KeyValue::TextTranslation.table_name
1413+
1414+
[string_table, text_table].each do |table_name|
1415+
next unless ActiveRecord::Base.connection.table_exists?(table_name)
1416+
1417+
# Build Arel query to check for translations safely
1418+
table = Arel::Table.new(table_name)
1419+
query = table.project(1)
1420+
.where(table[:translatable_type].eq(model_class.name))
1421+
.where(table[:key].eq(attribute_name))
1422+
.where(table[:locale].eq(locale))
1423+
.where(table[:value].not_eq(nil))
1424+
.where(table[:value].not_eq(''))
1425+
.take(1)
1426+
1427+
result = ActiveRecord::Base.connection.select_all(query.to_sql)
1428+
return true if result.rows.any?
1429+
1430+
# Check STI descendants if applicable
1431+
next unless model_class.respond_to?(:descendants) && model_class.descendants.any?
1432+
1433+
model_class.descendants.each do |subclass|
1434+
next unless subclass.respond_to?(:mobility_attributes)
1435+
1436+
descendant_query = table.project(1)
1437+
.where(table[:translatable_type].eq(subclass.name))
1438+
.where(table[:key].eq(attribute_name))
1439+
.where(table[:locale].eq(locale))
1440+
.where(table[:value].not_eq(nil))
1441+
.where(table[:value].not_eq(''))
1442+
.take(1)
1443+
1444+
result = ActiveRecord::Base.connection.select_all(descendant_query.to_sql)
1445+
return true if result.rows.any?
1446+
end
1447+
end
1448+
1449+
false
1450+
rescue StandardError => e
1451+
Rails.logger.warn("Error checking string/text translation for #{model_class.name}.#{attribute_name} in #{locale}: #{e.message}")
1452+
false
1453+
end
1454+
1455+
# Check if model has translation for specific Action Text attribute in given locale
1456+
def has_action_text_translation?(model_class, attribute_name, locale)
1457+
return false unless ActiveRecord::Base.connection.table_exists?('action_text_rich_texts')
1458+
1459+
# Build Arel query for Action Text translations
1460+
table = Arel::Table.new('action_text_rich_texts')
1461+
query = table.project(1)
1462+
.where(table[:record_type].eq(model_class.name))
1463+
.where(table[:name].eq(attribute_name))
1464+
.where(table[:locale].eq(locale))
1465+
.where(table[:body].not_eq(nil))
1466+
.where(table[:body].not_eq(''))
1467+
.take(1)
1468+
1469+
result = ActiveRecord::Base.connection.select_all(query.to_sql)
1470+
return true if result.rows.any?
1471+
1472+
# Check STI descendants
1473+
if model_class.respond_to?(:descendants) && model_class.descendants.any?
1474+
model_class.descendants.each do |subclass|
1475+
descendant_query = table.project(1)
1476+
.where(table[:record_type].eq(subclass.name))
1477+
.where(table[:name].eq(attribute_name))
1478+
.where(table[:locale].eq(locale))
1479+
.where(table[:body].not_eq(nil))
1480+
.where(table[:body].not_eq(''))
1481+
.take(1)
1482+
1483+
result = ActiveRecord::Base.connection.select_all(descendant_query.to_sql)
1484+
return true if result.rows.any?
1485+
end
1486+
end
1487+
1488+
false
1489+
rescue StandardError => e
1490+
Rails.logger.warn("Error checking Action Text translation for #{model_class.name}.#{attribute_name} in #{locale}: #{e.message}")
1491+
false
1492+
end
1493+
1494+
# Check if model has translation for specific Active Storage attachment in given locale
1495+
def has_active_storage_translation?(model_class, attachment_name, locale)
1496+
# For Active Storage, we need to check if there are attachments with the given locale
1497+
# Active Storage translations are typically handled through the KeyValue backend as well
1498+
# Let's check both mobility_string_translations and mobility_text_translations for active_storage keys
1499+
1500+
string_table = Mobility::Backends::ActiveRecord::KeyValue::StringTranslation.table_name
1501+
text_table = Mobility::Backends::ActiveRecord::KeyValue::TextTranslation.table_name
1502+
1503+
[string_table, text_table].each do |table_name|
1504+
next unless ActiveRecord::Base.connection.table_exists?(table_name)
1505+
1506+
# Build Arel query to check for Active Storage translations in KeyValue backend
1507+
table = Arel::Table.new(table_name)
1508+
query = table.project(1)
1509+
.where(table[:translatable_type].eq(model_class.name))
1510+
.where(table[:key].eq(attachment_name))
1511+
.where(table[:locale].eq(locale))
1512+
.where(table[:value].not_eq(nil))
1513+
.where(table[:value].not_eq(''))
1514+
.take(1)
1515+
1516+
result = ActiveRecord::Base.connection.select_all(query.to_sql)
1517+
return true if result.rows.any?
1518+
1519+
# Check STI descendants if applicable
1520+
next unless model_class.respond_to?(:descendants) && model_class.descendants.any?
1521+
1522+
model_class.descendants.each do |subclass|
1523+
descendant_query = table.project(1)
1524+
.where(table[:translatable_type].eq(subclass.name))
1525+
.where(table[:key].eq(attachment_name))
1526+
.where(table[:locale].eq(locale))
1527+
.where(table[:value].not_eq(nil))
1528+
.where(table[:value].not_eq(''))
1529+
.take(1)
1530+
1531+
result = ActiveRecord::Base.connection.select_all(descendant_query.to_sql)
1532+
return true if result.rows.any?
1533+
end
1534+
end
1535+
1536+
false
1537+
rescue StandardError => e
1538+
Rails.logger.warn("Error checking Active Storage translation for #{model_class.name}.#{attachment_name} in #{locale}: #{e.message}")
1539+
false
1540+
end
1541+
1542+
# Collect all translatable attributes for a model including backend types
1543+
def collect_model_translatable_attributes(model_class)
1544+
attributes = {}
1545+
1546+
# Check base model mobility attributes (align with helper logic)
1547+
if model_class.respond_to?(:mobility_attributes)
1548+
model_class.mobility_attributes.each do |attr|
1549+
# Try to get backend type from mobility config, default to :string
1550+
backend = :string
1551+
if model_class.respond_to?(:mobility) && model_class.mobility.attributes_hash[attr.to_sym]
1552+
backend = model_class.mobility.attributes_hash[attr.to_sym][:backend] || :string
1553+
end
1554+
attributes[attr.to_s] = backend
1555+
end
1556+
end
1557+
1558+
# Check Action Text attributes (already covered in the base model check above)
1559+
# No need to duplicate this check
1560+
1561+
# Check Active Storage attachments
1562+
if model_class.respond_to?(:mobility_translated_attachments) && model_class.mobility_translated_attachments&.any?
1563+
model_class.mobility_translated_attachments.each_key do |attachment|
1564+
attributes[attachment.to_s] = :active_storage
1565+
end
1566+
end
1567+
1568+
# Check STI descendants
1569+
if model_class.respond_to?(:descendants) && model_class.descendants.any?
1570+
model_class.descendants.each do |subclass|
1571+
# Mobility attributes
1572+
if subclass.respond_to?(:mobility_attributes)
1573+
subclass.mobility_attributes.each do |attr|
1574+
# Try to get backend type from mobility config, default to :string
1575+
backend = :string
1576+
if subclass.respond_to?(:mobility) && subclass.mobility.attributes_hash[attr.to_sym]
1577+
backend = subclass.mobility.attributes_hash[attr.to_sym][:backend] || :string
1578+
end
1579+
attributes[attr.to_s] = backend
1580+
end
1581+
end
1582+
1583+
# Active Storage attachments
1584+
unless subclass.respond_to?(:mobility_translated_attachments) && subclass.mobility_translated_attachments&.any?
1585+
next
1586+
end
1587+
1588+
subclass.mobility_translated_attachments.each_key do |attachment|
1589+
attributes[attachment.to_s] = :active_storage
1590+
end
1591+
end
1592+
end
1593+
1594+
attributes
1595+
end
1596+
1597+
# Calculate overall locale gap summary across all models
1598+
def calculate_locale_gap_summary
1599+
gap_summary = {}
1600+
1601+
I18n.available_locales.each do |locale|
1602+
locale_str = locale.to_s
1603+
gap_summary[locale_str] = {
1604+
total_models: 0,
1605+
models_with_gaps: 0,
1606+
total_missing_attributes: 0,
1607+
models_100_percent: 0
1608+
}
1609+
end
1610+
1611+
@model_instance_stats.each do |model_name, _stats|
1612+
begin
1613+
model_class = model_name.constantize
1614+
rescue NameError => e
1615+
Rails.logger.warn "Could not constantize model type #{model_name}: #{e.message}"
1616+
next
1617+
end
1618+
1619+
# Only calculate coverage for models that have translatable attributes
1620+
translatable_attributes = collect_model_translatable_attributes(model_class)
1621+
next if translatable_attributes.empty?
1622+
1623+
locale_coverage = calculate_locale_coverage_for_model(model_name, model_class)
1624+
1625+
locale_coverage.each do |locale_str, coverage|
1626+
gap_summary[locale_str][:total_models] += 1
1627+
gap_summary[locale_str][:total_missing_attributes] += coverage[:missing_attributes].length
1628+
1629+
if coverage[:missing_attributes].any?
1630+
gap_summary[locale_str][:models_with_gaps] += 1
1631+
else
1632+
gap_summary[locale_str][:models_100_percent] += 1
1633+
end
1634+
end
1635+
end
1636+
1637+
gap_summary
1638+
end
13561639
end
13571640
end

0 commit comments

Comments
 (0)