| \htmlhr |
| \chapterAndLabel{Internationalization Format String Checker (I18n Format String Checker)}{i18n-formatter-checker} |
| |
| The Internationalization Format String Checker, or I18n Format String Checker, |
| prevents use of incorrect i18n format strings. |
| |
| If the I18n Format String Checker issues no warnings or errors, then |
| \sunjavadoc{java.base/java/text/MessageFormat.html\#format(java.lang.String,java.lang.Object...)}{MessageFormat.format} |
| will raise no error at run time. |
| ``I18n'' is short for |
| ``internationalization'' because there are 18 characters between the ``i'' and |
| the ``n''. |
| |
| Here are the examples of errors that the |
| I18n Format Checker |
| detects at compile time. |
| |
| \begin{Verbatim} |
| // Warning: the second argument is missing. |
| MessageFormat.format("{0} {1}", 3.1415); |
| // String argument cannot be formatted as Time type. |
| MessageFormat.format("{0, time}", "my string"); |
| // Invalid format string: unknown format type: thyme. |
| MessageFormat.format("{0, thyme}", new Date()); |
| // Invalid format string: missing the right brace. |
| MessageFormat.format("{0", new Date()); |
| // Invalid format string: the argument index is not an integer. |
| MessageFormat.format("{0.2, time}", new Date()); |
| // Invalid format string: "#.#.#" subformat is invalid. |
| MessageFormat.format("{0, number, #.#.#}", 3.1415); |
| \end{Verbatim} |
| |
| For instructions on how to run the Internationalization Format String |
| Checker, see Section~\ref{i18n-format-running}. |
| |
| The Internationalization Checker or I18n Checker (\chapterpageref{i18n-checker}) |
| has a different purpose. It verifies that your code is properly |
| internationalized: any user-visible text should be obtained from a |
| localization resource and all keys exist in that resource. |
| |
| |
| \sectionAndLabel{Internationalization Format String Checker annotations}{i18n-format-annotation} |
| |
| |
| \begin{figure} |
| \includeimage{i18n-format-type-hierarchy}{3cm} |
| \caption{The |
| Internationalization |
| Format String Checker type qualifier hierarchy. |
| The type qualifiers are applicable to \<CharSequence> and its subtypes. |
| The figure does not show the subtyping rules among different |
| \refqualclass{checker/i18nformatter/qual}{I18nFormat}\code{(...)} |
| qualifiers; see |
| Section~\ref{i18n-format-conversion-categories}. |
| All \refqualclass{checker/i18nformatter/qual}{I18nFormatFor} annotations |
| are unrelated by subtyping, unless they are identical. |
| The qualifiers in gray are used internally by |
| the checker and should never be written by a programmer. |
| } |
| \label{i18n-format-type-hierarchy} |
| \end{figure} |
| |
| The \sunjavadoc{java.base/java/text/MessageFormat.html}{MessageFormat} documentation |
| specifies the syntax of the i18n format string. |
| |
| These are the qualifiers that make up the I18n Format String type system. |
| Figure~\ref{i18n-format-type-hierarchy} shows their subtyping relationships. |
| |
| \begin{description} |
| |
| \item[\refqualclass{checker/i18nformatter/qual}{I18nFormat}] |
| represents a valid i18n format string. For example, |
| \code{@I18nFormat(\{GENERAL, NUMBER, UNUSED, DATE\})} is a legal type for |
| \code{"\{0\}\{1, number\} \{3, date\}"}, indicating that when the format |
| string is used, |
| the first argument should be of \code{GENERAL} conversion category, |
| the second argument should be of \code{NUMBER} conversion category, and so on. |
| Conversion categories such as \code{GENERAL} are described in |
| Section~\ref{i18n-format-conversion-categories}. |
| |
| \item[\refqualclass{checker/i18nformatter/qual}{I18nFormatFor}] |
| indicates that the qualified type is a valid i18n format string for use |
| with some array of values. For example, |
| \code{@I18nFormatFor("\#2")} indicates that the string can be used to |
| format the contents of the second parameter array. |
| The argument is a Java expression whose syntax |
| is explained in Section~\ref{java-expressions-as-arguments}. |
| An example of its use is: |
| |
| \begin{Verbatim} |
| static void method(@I18nFormatFor("#2") String format, Object... args) { |
| // the body may use the parameters like this: |
| MessageFormat.format(format, args); |
| } |
| |
| method("{0, number} {1}", 3.1415, "A string"); // OK |
| // error: The string "hello" cannot be formatted as a Number. |
| method("{0, number} {1}", "hello", "goodbye"); |
| \end{Verbatim} |
| |
| \item[\refqualclass{checker/i18nformatter/qual}{I18nInvalidFormat}] |
| represents an invalid i18n format string. Programmers are not allowed to |
| write this annotation. It is only used internally by the type checker. |
| |
| \item[\refqualclass{checker/i18nformatter/qual}{I18nUnknownFormat}] |
| represents any string. The string might or might not be a valid i18n |
| format string. Programmers are not allowed to write this annotation. |
| |
| \item[\refqualclass{checker/i18nformatter/qual}{I18nFormatBottom}] |
| indicates that the value is definitely \<null>. Programmers are not allowed |
| to write this annotation. |
| \end{description} |
| |
| \sectionAndLabel{Conversion categories}{i18n-format-conversion-categories} |
| |
| In a message string, the optional second element within the curly braces is |
| called a \emph{format type} and must be one of \<number>, \<date>, |
| \<time>, and \<choice>. These four format types correspond to different |
| conversion categories. \<date> and \<time> correspond to \emph{DATE} in the |
| conversion categories figure. \<choice> corresponds to \emph{NUMBER}. |
| The format type restricts what arguments are legal. |
| For example, a date argument is not compatible with |
| the \<number> format type, i.e., \code{MessageFormat.format("\{0, number\}", |
| new Date())} will throw an exception. |
| |
| The I18n Checker represents the possible arguments via \emph{conversion |
| categories}. A conversion category defines a set of restrictions or a |
| subtyping rule. |
| |
| Figure~\ref{i18n-format-category} summarizes the subset |
| relationship among all conversion categories. |
| |
| \begin{figure} |
| \includeimage{i18n-format-category}{5cm} |
| \caption{The subset relationship among |
| i18n |
| conversion categories.} |
| \label{i18n-format-category} |
| \end{figure} |
| |
| |
| \sectionAndLabel{Subtyping rules for \<@I18nFormat>}{i18n-formatter-format-subtyping} |
| |
| Here are the subtyping rules among different |
| \code{@I18nFormat} |
| qualifiers. |
| It is legal to: |
| |
| \begin{itemize} |
| \item use a format string with a weaker (less restrictive) conversion category than required. |
| \item use a format string with fewer format specifiers than required. |
| Although this is legal a warning is issued because most occurrences of |
| this are due to programmer error. |
| \end{itemize} |
| |
| The following example shows the subtyping rules in action: |
| |
| \begin{Verbatim} |
| @I18nFormat({NUMBER, DATE}) String f; |
| |
| f = "{0, number, #.#} {1, date}"; // OK |
| f = "{0, number} {1}"; // OK, GENERAL is weaker (less restrictive) than DATE |
| f = "{0} {1, date}"; // OK, GENERAL is weaker (less restrictive) than NUMBER |
| f = "{0, number}"; // warning: last argument is ignored |
| f = "{0}"; // warning: last argument is ignored |
| f = "{0, number} {1, number}"; // error: NUMBER is stronger (more restrictive) than DATE |
| f = "{0} {1} {2}"; // error: too many arguments |
| \end{Verbatim} |
| |
| The conversion categories are: |
| |
| \begin{description} |
| |
| \item[\refenum{checker/i18nformatter/qual}{I18nConversionCategory}{UNUSED}{}] |
| indicates an unused argument. For example, in |
| \code{MessageFormat.format("\{0, number\} \{2, number\}", 3.14, "Hello", 2.718)} |
| , the second argument \code{Hello} is unused. Thus, the conversion |
| categories for the format, \code{{0, number} {2, number}}, is |
| \code{(NUMBER, UNUSED, NUMBER)}. |
| |
| \item[\refenum{checker/i18nformatter/qual}{I18nConversionCategory}{GENERAL}{}] |
| means that any value can be supplied as an argument. |
| |
| \item[\refenum{checker/i18nformatter/qual}{I18nConversionCategory}{DATE}{}] |
| is applicable for date, time, and number types. An argument needs to be |
| of \sunjavadoc{java.sql/java/sql/Date.html}{Date}, |
| \sunjavadoc{java.sql/java/sql/Time.html}{Time}, or |
| \sunjavadoc{java.base/java/lang/Number.html}{Number} type or a subclass of them, |
| including \sunjavadoc{java.sql/java/sql/Timestamp.html}{Timestamp} and the classes |
| listed immediately below. |
| |
| \item[\refenum{checker/i18nformatter/qual}{I18nConversionCategory}{NUMBER}{}] |
| means that the argument needs to be of \code{Number} |
| type or a subclass: |
| \sunjavadoc{java.base/java/lang/Number.html}{Number}, |
| \sunjavadoc{java.base/java/util/concurrent/atomic/AtomicInteger.html}{AtomicInteger}, |
| \sunjavadoc{java.base/java/util/concurrent/atomic/AtomicLong.html}{AtomicLong}, |
| \sunjavadoc{java.base/java/math/BigDecimal.html}{BigDecimal}, |
| \sunjavadoc{java.base/java/math/BigInteger.html}{BigInteger}, |
| \sunjavadoc{java.base/java/lang/Byte.html}{Byte}, |
| \sunjavadoc{java.base/java/lang/Double.html}{Double}, |
| \sunjavadoc{java.base/java/lang/Float.html}{Float}, |
| \sunjavadoc{java.base/java/lang/Integer.html}{Integer}, |
| \sunjavadoc{java.base/java/lang/Long.html}{Long}, |
| \sunjavadoc{java.base/java/lang/Short.html}{Short}. |
| |
| \end{description} |
| |
| \sectionAndLabel{What the Internationalization Format String Checker checks}{i18n-format-checks} |
| |
| The Internationalization Format String Checker checks calls to the i18n |
| formatting method \sunjavadoc{java.base/java/text/MessageFormat.html\#format(java.lang.String,java.lang.Object...)}{MessageFormat.format} |
| and guarantees the following: |
| |
| \begin{enumerate} |
| \item{The checker issues a warning for the following cases:} |
| \begin{enumerate} |
| \item There are missing arguments from what is required by the format string. |
| |
| \code{MessageFormat.format("\{0, number\} \{1, number\}", 3.14); // Output: 3.14 \{1\}} |
| |
| \item More arguments are passed than what is required by the format string. |
| |
| \code{MessageFormat.format("\{0, number\}", 1, new Date());} |
| |
| \code{MessageFormat.format("\{0, number\} \{0, number\}", 3.14, 3.14);} |
| |
| This does not cause an error at run time, but it often indicates a |
| programmer mistake. If it is intentional, then you should suppress |
| the warning (see Chapter~\ref{suppressing-warnings}). |
| |
| \item Some argument is an array of objects. |
| |
| \code{MessageFormat.format("\{0, number\} \{1\}", array);} |
| |
| The checker cannot verify whether the format string is valid, so |
| the checker conservatively issues a warning. This is a limitation of |
| the Internationalization Format String Checker. |
| |
| \end{enumerate} |
| \item The checker issues an error for the following cases: |
| \begin{enumerate} |
| \item The format string is invalid. |
| |
| \begin{itemize} |
| \item Unmatched braces. |
| |
| \code{MessageFormat.format("\{0, time", new Date());} |
| |
| \item The argument index is not an integer or is negative. |
| |
| \code{MessageFormat.format("\{0.2, time\}", new Date());} |
| |
| \code{MessageFormat.format("\{-1, time\}", new Date());} |
| |
| \item Unknown format type. |
| |
| \code{MessageFormat.format("\{0, foo\}", 3.14);} |
| |
| \item Missing a format style required for \<choice> format. |
| |
| \code{MessageFormat.format("\{0, choice\}", 3.14);} |
| |
| \item Wrong format style. |
| |
| \code{MessageFormat.format("\{0, time, number\}", 3.14);} |
| |
| \item Invalid subformats. |
| |
| \code{MessageFormat.format("\{0, number, \#.\#.\#\}", 3.14)} |
| \end{itemize} |
| |
| \item Some argument's type doesn't satisfy its conversion category. |
| |
| \code{MessageFormat.format("\{0, number\}", new Date());} |
| \end{enumerate} |
| \end{enumerate} |
| |
| The Checker also detects illegal assignments: assigning a non-format-string |
| or an incompatible format string to a variable declared as containing a |
| specific type of format string. For example, |
| |
| \begin{Verbatim} |
| @I18nFormat({GENERAL, NUMBER}) String format; |
| // OK. |
| format = "{0} {1, number}"; |
| // OK, GENERAL is weaker (less restrictive) than NUMBER. |
| format = "{0} {1}"; |
| // OK, it is legal to have fewer arguments than required (less restrictive). |
| // But the warning will be issued instead. |
| format = "{0}"; |
| |
| // Error, the format string is stronger (more restrictive) than the specifiers. |
| format = "{0} {1} {2}"; |
| // Error, the format string is more restrictive. NUMBER is a subtype of GENERAL. |
| format = "{0, number} {1, number}"; |
| \end{Verbatim} |
| |
| \sectionAndLabel{Resource files}{i18n-format-resource-files} |
| |
| A programmer rarely writes an i18n format string literally. (The examples |
| in this chapter show that for simplicity.) Rather, the i18n format strings are |
| read from a resource file. The program chooses a resource file at run time |
| depending on the locale (for example, different resource files for English |
| and Spanish users). |
| |
| \noindent For example, suppose that the \<resource1.properties> file contains |
| |
| \begin{Verbatim} |
| key1 = The number is {0, number}. |
| \end{Verbatim} |
| |
| \noindent Then code such as the following: |
| |
| \begin{Verbatim} |
| String formatPattern = ResourceBundle.getBundle("resource1").getString("key1"); |
| System.out.println(MessageFormat.format(formatPattern, 2.2361)); |
| \end{Verbatim} |
| |
| \noindent will output ``The number is 2.2361.'' A different resource file would contain |
| \code{key1 = El n\'{u}mero es \{0, number\}.} |
| |
| When you run the I18n Format String Checker, you need to indicate which resource file it |
| should check. If you change the resource file or use a different resource |
| file, you should re-run the checker |
| to ensure that you did not make an error. The I18n Format String Checker supports two types of |
| resource files: ResourceBundles and property files. The example above shows use of |
| resource bundles. |
| For more about checking property files, see \chapterpageref{propkey-checker}. |
| |
| |
| \sectionAndLabel{Running the Internationalization Format Checker}{i18n-format-running} |
| |
| The checker can be invoked by running one of the following commands (with |
| the whole command on one line). |
| |
| \begin{itemize} |
| \item Using ResourceBundles: |
| |
| \begin{smaller} |
| \code{javac -processor |
| org.checkerframework.checker.i18nformatter.I18nFormatterChecker |
| -Abundlenames=MyResource MyFile.java} |
| \end{smaller} |
| |
| \item Using property files: |
| |
| \begin{smaller} |
| \code{javac -processor |
| org.checkerframework.checker.i18nformatter.I18nFormatterChecker |
| -Apropfiles=MyResource.properties MyFile.java} |
| \end{smaller} |
| |
| \item Not using a property file. Use this if the programmer hard-coded the |
| format patterns without loading them from a property file. |
| |
| \begin{smaller} |
| \code{javac -processor |
| org.checkerframework.checker.i18nformatter.I18nFormatterChecker MyFile.java} |
| \end{smaller} |
| \end{itemize} |
| |
| |
| \sectionAndLabel{Testing whether a string has an i18n format type}{i18n-format-testing} |
| |
| In the case that the checker cannot infer the i18n format type of a string, |
| you can use the \refmethod{checker/i18nformatter/util}{I18nFormatUtil}{hasFormat}{-java.lang.String-org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory...-} |
| method to define the type of the string in the scope of a conditional statement. |
| |
| \begin{description} |
| |
| \item[\refmethod{checker/i18nformatter/util}{I18nFormatUtil}{hasFormat}{-java.lang.String-org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory...-}] |
| returns \<true> if the given string has the given i18n format type. |
| |
| \end{description} |
| |
| \noindent For an example, see Section~\ref{i18n-format-examples}. |
| |
| To use the \refclass{checker/i18nformatter/util}{I18nFormatUtil} class, the \<checker-util.jar> file |
| must be on the classpath at run time. |
| |
| |
| \sectionAndLabel{Examples of using the Internationalization Format Checker}{i18n-format-examples} |
| |
| \begin{itemize} |
| \item Using \sunjavadoc{java.base/java/text/MessageFormat.html\#format(java.lang.String,java.lang.Object...)}{MessageFormat.format}. |
| \begin{Verbatim} |
| // suppose the bundle "MyResource" contains: key1={0, number} {1, date} |
| String value = ResourceBundle.getBundle("MyResource").getString("key1"); |
| MessageFormat.format(value, 3.14, new Date()); // OK |
| // error: incompatible types in argument; found String, expected number |
| MessageFormat.format(value, "Text", new Date()); |
| \end{Verbatim} |
| \item Using the |
| \refmethod{checker/i18nformatter/util}{I18nFormatUtil}{hasFormat}{-java.lang.String-org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory...-} |
| method to check whether a format |
| string has particular conversion categories. |
| \begin{Verbatim} |
| void test1(String format) { |
| if (I18nFormatUtil.hasFormat(format, I18nConversionCategory.GENERAL, |
| I18nConversionCategory.NUMBER)) { |
| MessageFormat.format(format, "Hello", 3.14); // OK |
| // error: incompatible types in argument; found String, expected number |
| MessageFormat.format(format, "Hello", "Bye"); |
| // error: missing arguments; expected 2 but 1 given |
| MessageFormat.format(format, "Bye"); |
| // error: too many arguments; expected 2 but 3 given |
| MessageFormat.format(format, "A String", 3.14, 3.14); |
| } |
| } |
| \end{Verbatim} |
| \item Using \refqualclass{checker/i18nformatter/qual}{I18nFormatFor} |
| to ensure that an argument is a particular type of format string. |
| \begin{Verbatim} |
| static void method(@I18nFormatFor("#2") String f, Object... args) {...} |
| |
| // OK, MessageFormat.format(...) would return "3.14 Hello greater than one" |
| method("{0, number} {1} {2, choice,0#zero|1#one|1<greater than one}", |
| 3.14, "Hello", 100); |
| |
| // error: incompatible types in argument; found String, expected number |
| method("{0, number} {1}", "Bye", "Bye"); |
| \end{Verbatim} |
| \item Annotating a string with |
| \refqualclass{checker/i18nformatter/qual}{I18nFormat}. |
| \begin{Verbatim} |
| @I18nFormat({I18nConversionCategory.DATE}) String; |
| s1 = "{0}"; |
| s1 = "{0, number}"; // error: incompatible types in assignment |
| \end{Verbatim} |
| \end{itemize} |
| |
| %% LocalWords: I18n i18n java MessageFormat I18nFormat I18nFormatFor |
| %% LocalWords: arg I18nInvalid I18nUnknownFormat I18nFormatBottom |
| %% LocalWords: Timestamp AtomicInteger AtomicLong BigDecimal BigInteger |
| %% LocalWords: number' foo subformats resource1 key1 mero Abundlenames |
| %% LocalWords: ResourceBundles MyResource MyFile Apropfiles hasFormat |
| %% LocalWords: I18nFormatUtil formatter CharSequence I18nInvalidFormat |