The two mistakes that reach users most often in a translated app are a placeholder that does not match the source and a plural form that is missing. Each ecosystem writes both differently. This guide shows what GlobalLingo compares in each, with the mistakes from its fixtures.
String Catalogs, .strings and .stringsdict files use printf-style
format specifiers.
String(format:) and SwiftUI read the arguments in the order and with the types the specifiers
say, so a translation that reorders them without positions reads an integer as an object:
"%lld books on %@" : {
"localizations" : {
"en" : { "variations" : { "plural" : { "one" : { "stringUnit" : { "value" : "%1$lld book on %2$@" } }, … } } },
"fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ contient %lld livres" } }
}
}
ERROR FMT001 Sources/Shelf/Resources/Localizable.xcstrings:45 Placeholder mismatch
"%lld books on %@" in French (fr) uses %@ (an object) for argument 1; the source text has
%1$lld (a 64-bit integer)
The fix keeps the French word order and states the positions: "%2$@ contient %1$lld livres".
Sizes count too: %d reads 32 bits and %lld 64. A Turkish .stringsdict in
fixtures/uikit-strings reads the count with %1$d where the source
passes it with %1$lld, which prints a wrong number once the count no longer fits in 32 bits.
In a .stringsdict or a String Catalog with substitutions, the plural variants are substituted
into one format string, so GlobalLingo checks the translation as a whole: an argument counts as
used when any variant uses it, and %#@name@ takes the type of its rule
(NSStringFormatValueTypeKey, or formatSpecifier in a String Catalog; %arg stands for that
argument).
ARB messages use ICU MessageFormat. The names of the template's placeholders are the parameters of the generated method, so a translation must use the same names:
// app_en.arb (template)
"booksOnShelf": "{count, plural, =0{No books on {shelf}} one{One book on {shelf}} other{{count} books on {shelf}}}",
// app_de.arb
"booksOnShelf": "{count, plural, =0{Keine Bücher auf {regal}} one{Ein Buch auf {regal}} other{{count} Bücher auf {regal}}}"
flutter gen-l10n (Flutter 3.47.5) accepts this. It adds regal as a third parameter,
booksOnShelf(int count, String shelf, Object regal), to every locale's class, and the
existing call l10n.booksOnShelf(count, shelf) stops compiling. GlobalLingo reports it at the
ARB file:
ERROR FMT001 lib/l10n/app_de.arb:4 Placeholder mismatch
"booksOnShelf" in German (de) uses {regal}, which the source text does not have
WARNING FMT002 lib/l10n/app_de.arb:4 Placeholder dropped
"booksOnShelf" in German (de) leaves out {shelf} of the source text
Without use-escaping: true in l10n.yaml, gen_l10n treats apostrophes as plain text; with it,
'{' is a literal brace. GlobalLingo reads l10n.yaml and parses the same way.
i18next writes {{name}}, i18n-js %{name}, react-intl and other FormatJS libraries ICU's
{name}. When a translation renames a placeholder, i18next leaves the text as it is and the user
sees Hallo {{username}}; FormatJS reports that the value for username was not provided.
GlobalLingo reads the syntax from package.json (or jsonSyntax in
.globallingo.json) and compares names per plural form, so key_one may
say "one item" without {{count}}.
Languages split numbers into different groups. The Unicode CLDR defines the groups (categories)
per language; i18next and FormatJS select with these rules at run time through
Intl.PluralRules, and Apple's and Flutter's plural support uses the same category names.
| Language | Categories | Example numbers |
|---|---|---|
| English, German, Turkish | one, other | 1 / 0, 2, 5 |
| French, Spanish, Italian, Portuguese | one, many, other | many: 1,000,000 |
| Polish, Russian, Ukrainian | one, few, many, other | few: 2–4, 22–24; many: 0, 5–21 |
| Arabic | zero, one, two, few, many, other | all six |
| Japanese, Chinese, Korean | other | one form for every number |
A Polish translation with only one and other, copied from English, shows the other form
("5 pliku") where Polish needs many ("5 plików"). GlobalLingo lists the missing categories and
example numbers for each:
WARNING PLU001 src/locales/pl.json:6 Missing plural form
"cart.items" in Polish (pl) has no plural forms for few (e.g. 2, 3, 4), many (e.g. 0, 5, 6)
A translation that does not vary by plural at all, where the source does, gets the same finding
for every category besides other. Japanese, which has only other, does not. A plural without
other is an error (PLU002): it is the form for every number the other categories do not cover.
Explicit values such as =0 in ICU are extra forms for one number: they neither replace a
category nor count as missing.