The Dirigent project is a compact message formatting framework. It can be used similar to Java's Formatter system or printf-like systems. These systems seem to be made for programmers, but those working on these messages usually don't have programming experience and that should not be necessary. Dirigent focused providing the maximum of context to those how need it: Translators. All with the goal to improve translation quality in the long run. All macros (our term for text placeholders) don't use numbers or some weird %-syntax, but simple readable names that describe their content and fit into the message.
Beyond that, all macros are completely locale aware and with advanced features like custom formatters, additional context, message builders and post processors, the final output can be customized to any application. From simply inserting strings into a message all the way to complex object trees that carry consistent text styling information and other metadata.
A macro is a special sequence of letters which is replaced with an input parameter from Dirigent using a Formatter. It must be placed within a string message. Furthermore it's possible to specify more than one macro in a single message. In this context macros follow this syntax:
{[[<position>:]type[#<label>][:<args>]]}
or just {[position]}
Where [...]
denote optional parts and <...>
denote required parts.
Ensuing from the syntax here are the possibilities:
- Using default macros: "Hello {} {}, how are you doing?"; A default macro only uses a default formatter which can be specified with the Dirigent constructor. It doesn't provide any contextual information. The input parameters are inserted into the message in the same order as they're specified.
- Using indexed macros: "Hello {0} {1}, how are you doing?"; When translating a message with multiple arguments the translator might have to change the order in which the macros occur. This is a common problem as different languages tend to have different sentence structures. Therefore it must be possible to specify the position of the parameter in the input parameter list. Keep in mind positions do start with 0 not with 1! Simple indexed parameters use the default formatter as well.
- Using a different formatter: "Hello {name}, how are you doing?"; In this example the macro value will be generated by a formatter handling the name
name
. This formatter will receive the input value according to its position together with a composition context (containing for example locale information). The formatter will then produce it's most appropriate representation of the given input. - Using a different formatter and an index: "Hello {0:name}, how are you doing?"; It's also possible to add an optional position to the formatter. This has the same effect as for the default macro.
- Using a formatter with special arguments: "Date: {date:format=yyyy-MM-dd}"; A formatter can get different arguments to do the job. Every argument is split with a
:
. There are two types of different arguments. A parameter argument maps an argument name to a value like the example illustrates it. A value argument only has a value. This could be a flag. - Providing additional information: "Hello {0:name#name of the user:some-argument}, how are you doing?"; A label is a kind of contextual information for the localizer. This won't be available within the code and only helps the localizer to understand the meaning of the placeholder. A label can be used without specifying an argument, too.
The label as well as the arguments are allowed to contain any character except :
, }
and \
which have to be escaped using \
. A formatter name follows this rule but must respect the label separator #
additionally. An argument also must pay attention to the value separator =
which is used to separate the name of an argument from the respective value.
The Dirigent process can be started by calling one of the methods Dirigent#compose(Context, String, Object...)
or Dirigent#compose(String, Object...)
. The latter one will create an empty context and call the first method. The process consists of three independent steps:
- The first one divides the message into elements using a parser. Here are two different types. Text elements representing static text and macros which must be processed.
- The next step converts these elements into components. The main goal of this step is to resolve macros to their formatters.
ResolvedMacro
s will be joined by their formatter and matching input value,UnresolvableMacro
s will signal a missing formatter. Formatter can be registered at the Dirigent instance usingDirigent#registerFormatter(Formatter)
. To load the correct formatter for a macro, a formatter has a methodFormatter#getNames
returning a set of names of a macro triggering this formatter. Additionally theFormatter#isApplicable(Object)
method is used to check whether the formatter is able to handle the type of the message input value. If a macro doesn't have a name, a default formatter will be used which was specified at Dirigent creation time. By default it is theStringFormatter
, which is described below. This default formatter must handle all object types. TheFormatter#isApplicable(Object)
method is not checked at this point! An element will be converted into anUnresolvableMacro
component if a converter couldn't be found. This can have two reasons. The first reason is that there isn't any registered formatter handling the used name of the macro. The second one represents the case that there is a formatter for the macro, but it doesn't handle the actual type of the message input value. Both reasons are represented with aMacroResolutionState
. After converting an element to a component, the registeredPostProcessor
s of theDirigent
instance will be called. They are allowed to manipulate the components. More about it can be found in the PostProcessor section of this documentation. All the components will be grouped in a component group. - The last step composes these components into the final message. While the previous steps are already handled by the Dirigent library, this final step is up to you by sub-classing the
AbstractDirigent
class. The Dirigent framework provides theBuilderDirigent
implementation using aMessageBuilder
to compose the final message. This builder has two generic types. The type of the actual message and the type of the builder (a kind of intermediary object) to use. TheStringMessageBuilder
composesString
messages using aStringBuilder
. The components of a component group will be loaded and processed individually. The text ofText
components are appended without any modification. Resolved macro components are converted to another component by calling the actual formatter. Unresolved macro components are appended as a{{unresolved: <macro-name>}}
string. All other kind of components will result in an IllegalStateException. To change one of this behaviours the responsible method can be overwritten. In the end the final message object will be returned.
The Dirigent process can be started with a special compose context. This context includes information for the formatter and post processor which can be evaluated by them. The context is expandable dynamically. Specific entries relate to a specific ContextProperty
. This framework provides entries for a Locale
, a TimeZone
and a Currency
within the static context of the Contexts
helper class. Every ContextProperty
contains a DefaultProvider
which is used for getting a default value of the property if it isn't specified. To create a PropertyMapping
, which is necessary to create a compose context, the method ContextProperty#with(T)
can be used. The creation of a new context should be done by using the Contexts
class. Besides a few properties it provides methods for creating contexts.
Formatter are needed to format the messages input value. By default the Dirigent instance only has a default formatter, but macros having a name can't be processed. For that reason the Dirigent instance has a method called registerFormatter(Formatter)
which must be used to register a formatter formatting macros with specific names. Furthermore the default formatter can be overwritten by providing it at the Dirigent constructor.
Formatter
is an abstract class providing the functionality to handle post processors already. Every implementation must implement the method getNames
returning a set of strings representing the macro names which will be handled by the formatter, the method isApplicable(Object)
checks whether the specified object can be handled by this formatter and the method format(T, Context, Arguments)
returns a component representing the actual formatting result. The type parameter T
represents the message's input parameter. Context is the compose context and Arguments contains the arguments of the macro. The available implementations AbstractFormatter
and ReflectedFormatter
help implementing formatters faster. The AbstractFormatter
can be used to handle a specific object type like Integer
or Date
. The object type is read from the generic type of the class which is used for the implementation of the isApplicable(Object)
method. Furthermore a method for getNames
exists as well. The names must be provided as constructor parameters. The ReflectedFormatter
uses annotations to get the details. An implementation class must have the @Names
annotation at the class definition. Additionally it can provide several format methods having an object parameter and optionally a compose context and an arguments object. The methods must be marked with the Format
annotation. The ReflectedFormatter
checks the input parameter types and looks for the format implementation to use at runtime.
In addition to the described formatter, there are also ConstantFormatter
s. A constant formatter is special formatter type which doesn't consume any message input values. Instead it only uses the context and the macro arguments to produce its output.
The Dirigent framework provides a few formatters already. They all provide default macro names. Those can be changed by providing new names at the constructor.
The StringFormatter
can be used to format any object by calling the String#valueOf(Object)
method. This formatter is also used as the default formatter. It is possible to provide one of the flags uppercase
or lowercase
to return the uppercase or lowercase version of the string. The formatter only has one default name which is string
.
Example:
dirigent.compose("Hello {string}", "Stefan")
will result inHello Stefan
dirigent.compose("Hello {string:uppercase}", "Stefan")
will result inHello STEFAN
The NumberFormatter
can be used to format any Number
object. This is done by using a NumberFormat
. By default this class uses the NumberFormat#getInstance
method to format the number.
This behaviour can be changed by providing arguments. The parameter format
lets you specify an individual format which creates a DecimalFormat
instance. Additionally it's possible to specify one of the flags integer
, currency
, percent
. These result in of the methods NumberFormat#getIntegerInstance
, NumberFormat#getCurrencyInstance
and NumberFormat#getPercentInstance
.
Every instance will be created with the value of the property Contexts.LOCALE
which is specified in the compose context. Furthermore the value of Contexts.CURRENCY
will be considered and used as the Currency
of the NumberFormat
.
In addition it's possible to change the default behaviour of this formatter by specifying one of the modes INTEGER
, CURRENCY
and PERCENT
at the constructor. This causes that it represents the respective flag automatically.
The default names are number
, decimal
, double
and float
.
Example:
dirigent.compose("Result: {number}", 12345.344)
will result inResult: 12,345.344
with Localeen-US
dirigent.compose("Result: {number:format=#,###.0}", 36.4567)
will result inResult: 36.5
with Localeen-US
dirigent.compose("Result: {number:integer}", 36.4567)
will result inResult: 36
with Localeen-US
dirigent.compose("Result: {number:currency}", 12345)
will result inResult: $12,345.00
with Localeen-US
dirigent.compose("Result: {number:currency}", 12345)
will result inResult: EUR12,345.00
with Localeen-US
and Currency Eurodirigent.compose("Result: {number:percent}", 0.12)
will result inResult: 12%
with Localeen-US
The IntegerFormatter
overwrites the NumberFormat
and sets the mode to INTEGER
. The default names are integer
, long
, short
, amount
and count
.
Example:
dirigent.compose("Result: {integer}", 36.4567)
will result inResult: 36
with Localeen-US
The CurrencyFormatter
overwrites the NumberFormat
and sets the mode to CURRENCY
. The default names are currency
, money
and finance
.
Example:
dirigent.compose("Result: {currency}", 12345)
will result inResult: $12,345.00
with Localeen-US
dirigent.compose("Result: {currency}", 12345)
will result inResult: EUR12,345.00
with Localeen-US
and CurrencyEuro
The PercentFormatter
overwrites the NumberFormat
and sets the mode to PERCENT
. The default names are percent
and percentage
.
Example:
dirigent.compose("Result: {percent}", 0.12)
will result inResult: 12%
with Localeen-US
The DateTimeFormatter
can be used to format a Date
object. This is done by using a DateFormat
. The behaviour of this class must be specified using one of the modes DATE_TIME
, DATE
or TIME
. The first mode is responsible for formatting a date to a date and the time. The second mode only returns the date part and the last mode represents the time. In contrary to the NumberFormatter
here the mode must be specified to archive proper results. By default the DATE_TIME
mode is used.
The DATE_TIME
mode loads the formatter with the DateFormat.getDateTimeInstance
method. As styles it uses the default style. Of course this can be changed by specifying parameters. Does the macro contain one of the flags short
, medium
, long
or full
the related style will be used for both the date and the time. Additionally it's possible to style the two date parts separately. This can be done with the parameters date
and time
with a value which is similar to the flags.
The other two modes DATE
and TIME
result in a DateFormat
loaded with DateFormat.getDateInstance
or DateFormat.getTimeInstance
. The macro arguments work the same way. Of course here it doesn't make sense to provide one of the parameters date
and time
, the flag is enough, nevertheless it's possible.
Furthermore the format
parameter is supported as well. The format results in a respective SimpleDateFormat
which doesn't care about the actual mode of the formatter.
Every instance will be created with the value of the property Contexts.LOCALE
which is specified in the compose context. Furthermore the value of Contexts.TIMEZONE
will be considered and used as the TimeZone
of the DateFormat
.
The formatter only has one default name datetime
.
Example: assuming the date object represents May 25, 2017 3:13:21 PM UTC
dirigent.compose("Result: {datetime}", date)
will result inResult: May 25, 2017 3:13:21 PM
with Localeen-US
dirigent.compose("Result: {datetime}", date)
will result inResult: May 25, 2017 5:13:21 PM
with Localeen-US
and time zoneEurope/Berlin
dirigent.compose("Result: {datetime:full}", date)
will result inResult: Thursday, May 25, 2017 3:13:21 PM UTC
with Localeen-US
dirigent.compose("Result: {datetime:long}", date)
will result inResult: May 25, 2017 3:13:21 PM UTC
with Localeen-US
dirigent.compose("Result: {datetime:medium}", date)
will result inResult: May 25, 2017 3:13:21 PM
with Localeen-US
dirigent.compose("Result: {datetime:short}", date)
will result inResult: 5/25/17 3:13 PM
with Localeen-US
dirigent.compose("Result: {datetime:date=short:time=medium}", date)
will result inResult: 5/25/17 3:13:21 PM
with Localeen-US
dirigent.compose("Result: {datetime:short:time=medium}", date)
will result inResult: 5/25/17 3:13:21 PM
with Localeen-US
dirigent.compose("Result: {datetime:format=YYYY.MM.dd}", date)
will result inResult: 2017.05.25
with Localeen-US
dirigent.compose("Result: {datetime:format=YYYY.MM.dd HH\:mm\:ss}", date)
will result inResult: 2017.05.25 3:13:21
with Localeen-US
. Keep in mind that the:
character must be escaped, because otherwise it would be used to separate arguments.
The DateFormatter
overwrites the DateTimeFormatter
and sets the mode to DATE
. The default name is date
.
Example:
dirigent.compose("Result: {date}", date)
will result inResult: May 25, 2017
with Localeen-US
dirigent.compose("Result: {date:short}", date)
will result inResult: 5/25/17
with Localeen-US
dirigent.compose("Result: {date:format=HH:mm:ss}", date)
will result inResult: 15:13:21
with Localeen-US
The TimeFormatter
overwrites the DateTimeFormatter
and sets the mode to TIME
. The default name is time
.
Example:
dirigent.compose("Result: {time}", date)
will result inResult: 3:13:21 PM
with Localeen-US
dirigent.compose("Result: {time:short}", date)
will result inResult: 3:13 PM
with Localeen-US
dirigent.compose("Result: {time:format=YYYY.MM.dd}", date)
will result inResult: 2017.05.25
with Localeen-US
The StaticTextFormatter
is a constant formatter which doesn't consume an input parameter. Instead it only writes the text of the first argument directly to the message. This could be used to indicate text parts which shouldn't be formatted for example. The default name of the formatter is text
.
Example:
dirigent.compose("The value of the property is {text:true}")
will result inThe value of the property is true
dirigent.compose("This framework is {text:incredible awesome}")
will result inThis framework is incredible awesome
Pro tip: With post processors, which are described in the next section, this static text can be styled in a special way. For example it could be displayed bold or italic. This might be a use-case as well.
A post processor can be used to manipulate a macro after it was created by a formatter or by the Dirigent instance. Therefore the interface PostProcessor
provides a method process(Component, Context, Arguments)
getting the created component, the current compose context and the arguments of the macro. The result of the method is a component again. The input component will be replaced with the output component. They're allowed to be the same object of course.
An example use-case would be to add a color
option to every macro which allows to set the text color of the resulting text. Formatter specific post processors could be used to adapt pre-existing formatters to new component types.
The registration of post processors has handled on two levels:
- Globally using the method
Dirigent#addPostProcessor(PostProcessor)
- Per
Formatter
instance usingFormatter#addPostProcessor(PostProcessor)
Post processors, just like formatters, may return custom component implementations, but note that own implementations might require specific handling in the MessageBuilder
, so you have to overwrite it. Instead, using a TextComponent
(or the implementation Text
) or a ComponentGroup
might be enough as well. ComponentGroup
s allow arbitrary nesting of components and as such are very powerful.
The Dirigent frameworks provides a WrappingPostProcessor
wrapping an input component with static components using a ComponentGroup
.