@@ -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
13571640end
0 commit comments