@@ -36,6 +36,8 @@ pub struct LocalizationCatalogRecord {
3636 pub status : String ,
3737 #[ serde( skip_serializing_if = "Option::is_none" ) ]
3838 pub comment : Option < String > ,
39+ #[ serde( default , skip_serializing_if = "BTreeMap::is_empty" ) ]
40+ pub translations : BTreeMap < String , String > ,
3941}
4042
4143#[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
@@ -123,6 +125,7 @@ pub struct LocalizationCatalogIndex {
123125 records : Vec < LocalizationCatalogRecord > ,
124126 by_table_key : HashMap < ( String , String ) , Vec < usize > > ,
125127 by_key : HashMap < String , Vec < usize > > ,
128+ by_value : HashMap < String , Vec < usize > > ,
126129}
127130
128131impl LocalizationCatalogIndex {
@@ -136,6 +139,17 @@ impl LocalizationCatalogIndex {
136139 . entry ( record. key . clone ( ) )
137140 . or_default ( )
138141 . push ( index) ;
142+ if !record. source_value . is_empty ( ) {
143+ self . by_value
144+ . entry ( record. source_value . clone ( ) )
145+ . or_default ( )
146+ . push ( index) ;
147+ }
148+ for value in record. translations . values ( ) {
149+ if !value. is_empty ( ) {
150+ self . by_value . entry ( value. clone ( ) ) . or_default ( ) . push ( index) ;
151+ }
152+ }
139153 self . records . push ( record) ;
140154 }
141155
@@ -167,6 +181,16 @@ impl LocalizationCatalogIndex {
167181 . cloned ( )
168182 . collect ( )
169183 }
184+
185+ pub fn records_for_value ( & self , value : & str ) -> Vec < LocalizationCatalogRecord > {
186+ self . by_value
187+ . get ( value)
188+ . into_iter ( )
189+ . flatten ( )
190+ . filter_map ( |index| self . records . get ( * index) )
191+ . cloned ( )
192+ . collect ( )
193+ }
170194}
171195
172196pub fn build_and_save_catalog_snapshot (
@@ -240,6 +264,23 @@ fn build_catalog_snapshot(
240264 let catalog_file = path_relative_to_root ( & root, & catalog. path ) ;
241265 let catalog_dir = path_to_snapshot_string ( & path_relative_to_root ( & root, & catalog. base_dir ) ) ;
242266
267+ let mut translations_by_key: HashMap < String , BTreeMap < String , String > > = HashMap :: new ( ) ;
268+ for resource in & codec. resources {
269+ let lang = & resource. metadata . language ;
270+ if lang == & source_language {
271+ continue ;
272+ }
273+ for entry in & resource. entries {
274+ let value = translation_plain_string ( & entry. value ) ;
275+ if !value. is_empty ( ) {
276+ translations_by_key
277+ . entry ( entry. id . clone ( ) )
278+ . or_default ( )
279+ . insert ( lang. clone ( ) , value) ;
280+ }
281+ }
282+ }
283+
243284 for entry in & source_resource. entries {
244285 records. push ( LocalizationCatalogRecord {
245286 table : table. clone ( ) ,
@@ -253,6 +294,9 @@ fn build_catalog_snapshot(
253294 . trim_matches ( '"' )
254295 . to_string ( ) ,
255296 comment : entry. comment . clone ( ) ,
297+ translations : translations_by_key
298+ . remove ( & entry. id )
299+ . unwrap_or_default ( ) ,
256300 } ) ;
257301 }
258302 }
@@ -1053,6 +1097,108 @@ mod tests {
10531097 ) ;
10541098 }
10551099
1100+ #[ test]
1101+ fn lookup_by_source_value ( ) {
1102+ let index = LocalizationCatalogIndex :: from_records ( vec ! [ LocalizationCatalogRecord {
1103+ table: "Localizable" . to_string( ) ,
1104+ key: "entrance_effect" . to_string( ) ,
1105+ catalog_file: "Localizable.xcstrings" . to_string( ) ,
1106+ catalog_dir: "." . to_string( ) ,
1107+ source_language: "zh-Hans" . to_string( ) ,
1108+ source_value: "进场特效" . to_string( ) ,
1109+ status: "translated" . to_string( ) ,
1110+ comment: None ,
1111+ translations: BTreeMap :: new( ) ,
1112+ } ] ) ;
1113+
1114+ assert ! ( index. records_for_key( "进场特效" ) . is_empty( ) ) ;
1115+
1116+ let records = index. records_for_value ( "进场特效" ) ;
1117+ assert_eq ! ( records. len( ) , 1 ) ;
1118+ assert_eq ! ( records[ 0 ] . key, "entrance_effect" ) ;
1119+ }
1120+
1121+ #[ test]
1122+ fn lookup_by_translation_value ( ) {
1123+ let mut translations = BTreeMap :: new ( ) ;
1124+ translations. insert ( "zh-Hans" . to_string ( ) , "欢迎" . to_string ( ) ) ;
1125+ translations. insert ( "fr" . to_string ( ) , "Bienvenue" . to_string ( ) ) ;
1126+
1127+ let index = LocalizationCatalogIndex :: from_records ( vec ! [ LocalizationCatalogRecord {
1128+ table: "Localizable" . to_string( ) ,
1129+ key: "welcome_title" . to_string( ) ,
1130+ catalog_file: "Localizable.xcstrings" . to_string( ) ,
1131+ catalog_dir: "." . to_string( ) ,
1132+ source_language: "en" . to_string( ) ,
1133+ source_value: "Welcome" . to_string( ) ,
1134+ status: "translated" . to_string( ) ,
1135+ comment: None ,
1136+ translations,
1137+ } ] ) ;
1138+
1139+ assert ! ( index. records_for_key( "欢迎" ) . is_empty( ) ) ;
1140+
1141+ let records = index. records_for_value ( "欢迎" ) ;
1142+ assert_eq ! ( records. len( ) , 1 ) ;
1143+ assert_eq ! ( records[ 0 ] . key, "welcome_title" ) ;
1144+
1145+ let records = index. records_for_value ( "Bienvenue" ) ;
1146+ assert_eq ! ( records. len( ) , 1 ) ;
1147+ assert_eq ! ( records[ 0 ] . key, "welcome_title" ) ;
1148+
1149+ let records = index. records_for_value ( "Welcome" ) ;
1150+ assert_eq ! ( records. len( ) , 1 ) ;
1151+ assert_eq ! ( records[ 0 ] . key, "welcome_title" ) ;
1152+ }
1153+
1154+ #[ test]
1155+ fn snapshot_stores_non_source_translations ( ) {
1156+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1157+ let store_dir = dir. path ( ) . join ( ".grapha" ) ;
1158+ let file = dir. path ( ) . join ( "Localizable.xcstrings" ) ;
1159+ fs:: write (
1160+ & file,
1161+ r#"{
1162+ "sourceLanguage" : "en",
1163+ "strings" : {
1164+ "welcome_title" : {
1165+ "localizations" : {
1166+ "en" : {
1167+ "stringUnit" : {
1168+ "state" : "translated",
1169+ "value" : "Welcome"
1170+ }
1171+ },
1172+ "zh-Hans" : {
1173+ "stringUnit" : {
1174+ "state" : "translated",
1175+ "value" : "欢迎"
1176+ }
1177+ }
1178+ }
1179+ }
1180+ },
1181+ "version" : "1.0"
1182+ }"# ,
1183+ )
1184+ . unwrap ( ) ;
1185+
1186+ build_and_save_catalog_snapshot ( dir. path ( ) , & store_dir) . unwrap ( ) ;
1187+ let index = load_catalog_index_from_store ( & store_dir) . unwrap ( ) ;
1188+
1189+ let records = index. records_for_key ( "welcome_title" ) ;
1190+ assert_eq ! ( records. len( ) , 1 ) ;
1191+ assert_eq ! ( records[ 0 ] . source_value, "Welcome" ) ;
1192+ assert_eq ! (
1193+ records[ 0 ] . translations. get( "zh-Hans" ) . map( String :: as_str) ,
1194+ Some ( "欢迎" )
1195+ ) ;
1196+
1197+ let by_value = index. records_for_value ( "欢迎" ) ;
1198+ assert_eq ! ( by_value. len( ) , 1 ) ;
1199+ assert_eq ! ( by_value[ 0 ] . key, "welcome_title" ) ;
1200+ }
1201+
10561202 #[ test]
10571203 fn builds_and_loads_strings_catalogs ( ) {
10581204 let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
@@ -1223,6 +1369,7 @@ mod tests {
12231369 source_value: "Auth" . to_string( ) ,
12241370 status: "translated" . to_string( ) ,
12251371 comment: None ,
1372+ translations: BTreeMap :: new( ) ,
12261373 } ,
12271374 LocalizationCatalogRecord {
12281375 table: "Localizable" . to_string( ) ,
@@ -1233,6 +1380,7 @@ mod tests {
12331380 source_value: "Profile" . to_string( ) ,
12341381 status: "translated" . to_string( ) ,
12351382 comment: None ,
1383+ translations: BTreeMap :: new( ) ,
12361384 } ,
12371385 ] ;
12381386
@@ -1256,6 +1404,7 @@ mod tests {
12561404 source_value: "A" . to_string( ) ,
12571405 status: "translated" . to_string( ) ,
12581406 comment: None ,
1407+ translations: BTreeMap :: new( ) ,
12591408 } ,
12601409 LocalizationCatalogRecord {
12611410 table: "Localizable" . to_string( ) ,
@@ -1266,6 +1415,7 @@ mod tests {
12661415 source_value: "B" . to_string( ) ,
12671416 status: "translated" . to_string( ) ,
12681417 comment: None ,
1418+ translations: BTreeMap :: new( ) ,
12691419 } ,
12701420 ] ;
12711421
@@ -1370,6 +1520,7 @@ mod tests {
13701520 source_value: "Welcome" . to_string( ) ,
13711521 status: "translated" . to_string( ) ,
13721522 comment: None ,
1523+ translations: BTreeMap :: new( ) ,
13731524 } ] ) ;
13741525
13751526 let resolution = resolve_usage (
@@ -1482,6 +1633,7 @@ mod tests {
14821633 source_value: "Share room" . to_string( ) ,
14831634 status: "translated" . to_string( ) ,
14841635 comment: None ,
1636+ translations: BTreeMap :: new( ) ,
14851637 } ] ) ;
14861638
14871639 let resolution = resolve_usage (
@@ -1603,6 +1755,7 @@ mod tests {
16031755 source_value: "The ID you entered does not exist" . to_string( ) ,
16041756 status: "translated" . to_string( ) ,
16051757 comment: None ,
1758+ translations: BTreeMap :: new( ) ,
16061759 } ,
16071760 LocalizationCatalogRecord {
16081761 table: "Localizable" . to_string( ) ,
@@ -1613,6 +1766,7 @@ mod tests {
16131766 source_value: "List is empty" . to_string( ) ,
16141767 status: "translated" . to_string( ) ,
16151768 comment: None ,
1769+ translations: BTreeMap :: new( ) ,
16161770 } ,
16171771 ] ) ;
16181772
@@ -1675,6 +1829,7 @@ mod tests {
16751829 source_value: "Tournament" . to_string( ) ,
16761830 status: "translated" . to_string( ) ,
16771831 comment: None ,
1832+ translations: BTreeMap :: new( ) ,
16781833 } ] ) ;
16791834
16801835 let resolution = resolve_usage (
0 commit comments