Multilingual decision table

From FlexRule Wiki
Jump to: navigation, search

Scope

When business rules are used in many different locales (countries in which different languages are spoken), then it make sense to model and design them in a way that multilingual concepts apply, on the following topics:

  1. Notification messages
  2. Values (string values)
  3. Options (option for values in glossary)

Let's consider the following simple business rules:

If Accounting Concept is Payable
then write Account concept is cash

As it is highlighted, the two parts of the rule must support multilingual.

Sample business rules

Let's assume we want to model this rule in two languages - English and Spanish.

English

Let's model this rule in a decision table that has an input parameter named ac:

En-accounting-dt.png

In XML this will be modeled as follows:

  1. <DecisionTable name="cash decision table">
  2.     <Columns>
  3.         <Condition name="Accounting Concept" expression="ac == $value" />
  4.         <Action name="Write Message" expression="$value" type="Notice" />
  5.     </Columns>
  6.     <Data>
  7.         <Row>
  8.             <Value>Payable</Value>
  9.             <Value>Account concept is cash</Value>
  10.         </Row>
  11.     </Data>
  12. </DecisionTable>

Spanish

Let's model this rule in a Decision Table that has an input parameter named ac:

Es-accounting-dt.png

In XML, this will be modeled as follows:

  1. <DecisionTable name="cash decision table">
  2.     <Columns>
  3.         <Condition name="Concepto de Contabilidad" expression="ac == $value" />
  4.         <Action name="Escribe un mensaje" expression="$value" type="Notice" />
  5.     </Columns>
  6.     <Data>
  7.         <Row>
  8.             <Value>Pagadero</Value>
  9.             <Value>Concepto de cuentas es dinero en efectivo</Value>
  10.         </Row>
  11.     </Data>
  12. </DecisionTable>

Concept

Values

In both rules, in the condition column:

  • Payable
  • Pagadero

are both referring to the same thing, but in different languages. In resolving the values on evaluation, all translations (e.g., English, Spanish, German, etc.) must be resolved against the same reference value. This reference value can be either a key (e.g., code, id, number, etc.) or a default main locale (e.g., in English, which is Payable).

Let's say we are going to assume 1 is an identifier or code we want to be resolved for all the translations of Payable.

  1. const string payableValue = "1";
  2. var termsTranslation = new NameValueCollection
  3. {
  4.     {"Payable", payableValue},
  5.     {"Pagadero", payableValue}
  6. };

As you can see, at this stage all of the translations of the Payable term are registered against payableValue.

When the engine (i.e. RuntimeEngine) is created, simply use the Entries property and add these translations:

  1. foreach (var term in termsTranslation.AllKeys)
  2.     engine.Entries.Add(term, string.Format("'{0}'", termsTranslation[term]), false);

Messages

Notification is smart and will simply pick the correct message based on the current Thread's UI Culture. Therefore, all of the messages must be registered for the target culture:

  1. var msg1_spanish = new Dictionary<string, string> { { "MSG_1", "Concepto de cuentas es dinero en efectivo" } };
  2. var msg1_english = new Dictionary<string, string> { { "MSG_1", "Account concept is cash" } };
  3. engine.OnRunning = (e) =>
  4. {
  5.     e.ExecutorSetup.MultilingualMessages.Register(msg1_spanish, CultureInfo.GetCultureInfo("es-ES"));
  6.     e.ExecutorSetup.MultilingualMessages.Register(msg1_english, CultureInfo.GetCultureInfo("en-US"));
  7. };

Please note that in this example, MSG_1 is the key to different translations of the message corresponding to the business rule. Make sure in your decision table you set useMessageId, or if you use FlexRule Designer then you need to set it as an action of the Decision Table:

Frd-UseMssageId.png

Final Decision Table

In the final Decision Table we do not add a direct message, instead we will be using a message code or id. Now for the values, all the translations of the term (i.e., Payable) will be accepted.

En-accounting-dt-final.png

Putting it all together

  1. public void test_multilingual_value_and_message()
  2. {
  3.     const string dt = @"
  4.     <DecisionTable name=""DT1"">
  5.       <Declaration>
  6.         <Define direction=""in"" name=""ac"" />
  7.       </Declaration>
  8.       <Columns>
  9.         <Condition name=""Accounting Concept"" expression=""ac==$value"" />
  10.         <Action name=""Write Message""  expression=""$value"" type=""notice"" notice=""information"" useMessageId=""true""/>
  11.       </Columns>
  12.       <Data>
  13.         <Row>
  14.           <Value>Payable</Value>
  15.           <Value>MSG_1</Value>
  16.         </Row>
  17.       </Data>
  18.     </DecisionTable>
  19.     ";
  20.  
  21.     var engine = RuntimeEngine.FromXml(Encoding.UTF8.GetBytes(dt));
  22.  
  23.     // Register all terms' value translations
  24.     const string payableValue = "1";
  25.     var termsTranslation = new NameValueCollection
  26.     {
  27.         {"Payable", payableValue},
  28.         {"Pagadero", payableValue}
  29.     };
  30.  
  31.     foreach (var term in termsTranslation.AllKeys)
  32.         engine.Entries.Add(term, string.Format("'{0}'", termsTranslation[term]), false);
  33.  
  34.  
  35.     // Register notifications messages for different culture:
  36.     // load messageId and translation for Spanish
  37.     var msg1_spanish = new Dictionary<string, string> { { "MSG_1", "Concepto de cuentas es dinero en efectivo" } };
  38.     engine.RegisterMultilingualMessage(msg1_spanish, CultureInfo.GetCultureInfo("es-ES"));
  39.  
  40.     // load messageId and translation for English
  41.     var msg1_english = new Dictionary<string, string> { { "MSG_1", "Account concept is cash" } };
  42.     engine.RegisterMultilingualMessage(msg1_english, CultureInfo.GetCultureInfo("en-US"));
  43.  
  44.  
  45.     System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
  46.     var result = engine.Run(termsTranslation["Payable"]);
  47.     Assert.AreEqual(msg1_english["MSG_1"], result.Notifications.Default.Notices.First().Message);
  48.     result = engine.Run(termsTranslation["Pagadero"]);
  49.     Assert.AreEqual(msg1_english["MSG_1"], result.Notifications.Default.Notices.First().Message);
  50.  
  51.     System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("es-ES");
  52.     result = engine.Run(termsTranslation["Pagadero"]);
  53.     Assert.AreEqual(msg1_spanish["MSG_1"], result.Notifications.Default.Notices.First().Message);
  54.     result = engine.Run(termsTranslation["Payable"]);
  55.     Assert.AreEqual(msg1_spanish["MSG_1"], result.Notifications.Default.Notices.First().Message);
  56. }

Business Glossary

A business glossary is a dictionary of set of your business terminologies, definitions, synonyms and translations. With the use of the business glossary, the rule integration, packaging, deployment and integration of multilingual values are streamlined.

Sample

Let's assume we have the following Decision Table and glossary defined and we load it as a ruleset:

  1. private static IRuleSet GetAccountingRuleSet()
  2. {
  3.     var mc = new ModelContainer();
  4.  
  5.     const string dt = @"
  6.     <DecisionTable name=""cash decision table"">
  7.         <Declaration>
  8.             <Define direction=""in"" name=""ac"" />
  9.         </Declaration>
  10.         <Glossary>
  11.             <GlossarySource uri=""ruleset:///accounting terms""/>
  12.         </Glossary>
  13.         <Columns>
  14.             <Condition name=""Accounting Concept"" term=""Payable"" />
  15.         <Action name=""Write Message""  expression=""$value"" type=""notice"" notice=""information"" useMessageId=""true""/>
  16.         </Columns>
  17.         <Data>
  18.             <Row>
  19.                 <Value>Payable</Value>
  20.                 <Value>MSG_1</Value>
  21.             </Row>
  22.         </Data>
  23.     </DecisionTable>
  24.     ";
  25.     LoadAdapterUtility.FillNavigableSource(mc, Encoding.UTF8.GetBytes(dt));
  26.  
  27.     const string gl = @"
  28.     <Glossary name=""accounting terms"">
  29.         <Term name=""Payable"" expression=""ac==$value"">
  30.             <Synonym value=""Pagadero""/>
  31.         </Term>
  32.     </Glossary>
  33.     ";
  34.     LoadAdapterUtility.FillNavigableSource(mc, Encoding.UTF8.GetBytes(gl));
  35.  
  36.     var rs = RuleSet.HierarchicalRuleSet();
  37.     rs.AddModel("", mc);
  38.     return rs;
  39. }

The Decision Table is the same as we discussed earlier, but in addition it has a reference to a defined glossary.

Preloaded glossary

When your application maintains a business glossary outside of the rule definition, this approach allows you to load the business glossary from any external data source and inject it during business rule load and execution.

  1. var rs = GetAccountingRuleSet();
  2. const string rsUri = "ruleset:///cash decision table";
  3.  
  4. // load glossary from ruleset
  5. var glossary = Glossary.Load(rs.SelectFirst(rsUri), rs);
  6.  
  7. // create engine instance and provide the pre-loaded glossary
  8. var engine = RuntimeEngine.FromRuleSet(rs, rsUri, glossary);
  9.  
  10. // Register all terms' value translations
  11. engine.Entries.ImportFromGlossary(glossary);

Embedded glossary

In this approach, the engine loads the glossary from the ruleset that is referenced in the Decision Table automatically.

  1. var rs = GetAccountingRuleSet();
  2. const string rsUri = "ruleset:///cash decision table";
  3.  
  4. // create engine instance and let the embedded glossary load and be utilised
  5. var engine = RuntimeEngine.FromRuleSet(rs, rsUri);
  6.  
  7. // Get the glossary from the loaded ruleset
  8. var glossary = Glossary.Load(rs.SelectFirst(rsUri), rs);
  9. // Register all terms' value translations
  10. engine.Entries.ImportFromGlossary(glossary);

Multilingual using Business Glossary Sample

The example below shows how to use the business glossary to make values multilingual in a sample Decision Table:

  1. [TestMethod]
  2. public void test_multilingual_load_glossary_onload()
  3. {
  4.     var rs = GetAccountingRuleSet();
  5.     const string rsUri = "ruleset:///cash decision table";
  6.  
  7.     var engine = RuntimeEngine.FromRuleSet(rs, rsUri);
  8.  
  9.     var glossary = Glossary.Load(rs.SelectFirst(rsUri), rs);
  10.     // Register all terms' value translations
  11.     engine.Entries.ImportFromGlossary(glossary);
  12.  
  13.     // Register notifications
  14.     var msg1_spanish = new Dictionary<string, string> { { "MSG_1", "Concepto de cuentas es dinero en efectivo" } };
  15.     engine.RegisterMultilingualMessage(msg1_spanish, CultureInfo.GetCultureInfo("es-ES"));
  16.  
  17.     var msg1_english = new Dictionary<string, string> { { "MSG_1", "Account concept is cash" } };
  18.     engine.RegisterMultilingualMessage(msg1_english, CultureInfo.GetCultureInfo("en-US"));
  19.  
  20.  
  21.     System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
  22.     var result = engine.Run(glossary.Lookup("Payable"));
  23.     Assert.AreEqual(msg1_english["MSG_1"], result.Notifications.Default.Notices.First().Message);
  24.     result = engine.Run(glossary.Lookup("Pagadero"));
  25.     Assert.AreEqual(msg1_english["MSG_1"], result.Notifications.Default.Notices.First().Message);
  26.  
  27.     System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("es-ES");
  28.     result = engine.Run(glossary.Lookup("Pagadero"));
  29.     Assert.AreEqual(msg1_spanish["MSG_1"], result.Notifications.Default.Notices.First().Message);
  30.     result = engine.Run(glossary.Lookup("Payable"));
  31.     Assert.AreEqual(msg1_spanish["MSG_1"], result.Notifications.Default.Notices.First().Message);
  32. }