Every finding names its rule. The severity is the default; rules in
.globallingo.json changes it or turns a rule off. Each rule lists what it
checks, why it matters, how to fix a finding in each ecosystem and the documents it is based on.
| Rule | Severity | Finding |
|---|---|---|
| MIS001 | error | Missing translation |
| MIS002 | error | Empty translation |
| MIS003 | warning | Needs review |
| MIS004 | info | Same as the source |
| STA001 | warning | Stale key |
| STA002 | warning | Key not in the source language |
| FMT001 | error | Placeholder mismatch |
| FMT002 | warning | Placeholder dropped |
| FMT003 | error | Invalid syntax |
| PLU001 | warning | Missing plural form |
| PLU002 | error | Plural without "other" |
| DUP001 | error | Duplicate key |
| KEY001 | error | Unknown key |
| KEY002 | warning | SwiftUI text not in the String Catalog |
| KEY003 | info | Unused key |
| HCS001 | warning | Hard-coded UI text |
| TRI001 | warning | Locale-dependent case change on a key |
| TRI002 | warning | Dotted or dotless I in a key |
| TRI003 | warning | Translated text cased without the locale |
The locales a catalog is checked for are the ones it has: the localizations in a String Catalog,
the .lproj folders of a strings table, the ARB files of an arb-dir, the JSON files (or locale
folders) of a translation folder. locales in the configuration, or --locales, narrows the list
and reports a listed locale that has no file. Base, mul and und are never targets.
Missing translation · error
A key of the source language has no translation in a locale the catalog has. In a String Catalog
a localization in state new also counts as missing: Xcode creates it when a string is added and
nobody has translated it yet.
What users see depends on the library: Foundation returns the key or the default value given in
code, gen_l10n uses the template's message, i18next and i18n-js fall back to the fallback
language or show the key, react-intl shows the defaultMessage or the id.
.strings file.Sources: Localizing and varying text with a String Catalog, Flutter internationalization.
Empty translation · error
The translation is "" while the source text is not. Xcode, gen_l10n and i18next use an empty
value as it is, so the label or button shows nothing. Write the translation, or remove the empty
entry so the fallback is used until someone translates it.
Needs review · warning
A String Catalog localization is in state needs_review (Xcode sets it when the source text
changes after translation) or stale. Check it against the current source text and mark it as
reviewed in the String Catalog editor.
Same as the source · info
The translation is identical to the source text (and has at least three letters), which often
means it was copied and never translated. Locales of the source language (en-GB for en) are
left out. Brand names and words that are the same in both languages belong in ignore.keys.
Stale key · warning
Xcode marks a String Catalog key "extractionState": "stale" when it no longer finds the string
in the source code. The catalog still ships it and translators still see it. Delete it (the
editor's Remove Stale Strings) or mark it as manually managed if the code builds the key at run
time.
Key not in the source language · warning
A translation has a key that the source language file does not: a leftover after a rename, or a
typo in the key. The app never asks for it. Delete it, or add it to the source language if the
app uses it. Strings tables of storyboards and XIBs (no source .strings file) are not checked.
GlobalLingo compares placeholders the way each runtime reads them:
.strings, .stringsdict) by argument position and
type: %@ is an object, %d a 32-bit integer, %ld and %lld 64-bit integers, %f a
double. %1$@ and %2$lld give positions explicitly; %#@name@ refers to a plural rule whose
NSStringFormatValueTypeKey (or formatSpecifier in a String Catalog) gives the type, and
%arg in a String Catalog substitution stands for that argument.{name},
{count, plural, …}, {gender, select, …}.{{name}} (also {{name, format}} and {{- name}}), i18n-js by %{name}.Placeholder mismatch · error
The translation uses a placeholder the source text does not have, or reads an argument as
another type. With format specifiers this is a run-time fault: %@ given an integer reads it as
an object pointer, %d given a 64-bit integer prints the wrong number. With named placeholders
the value is not passed: FormatJS reports a missing value, i18next leaves {{username}} in the
text, and gen_l10n adds the unknown name as a new parameter of the generated method, so the code
that calls it stops compiling (see fixtures/flutter-arb).
"%2$@ rafında %1$lld kitap".@key metadata.Sources: String Format Specifiers, ICU message syntax (FormatJS), i18next interpolation.
Placeholder dropped · warning
The translation leaves out a placeholder of the source text, so a name or a count is not shown. Plural forms for zero, one and two may leave the number out ("one file", "un fichier"), so those forms are not reported. For format specifiers the whole translation counts: an argument is used when any of its plural variants uses it.
Invalid syntax · error
A file does not parse (broken JSON, an unterminated .strings entry, a malformed .stringsdict),
or a message is not valid in its syntax: an unbalanced brace or an unknown plural category in
ICU, or a % followed by a letter that is no conversion in a format string (write %% for a
percent sign). Code generation fails (gen_l10n), the library throws or prints the raw text
(FormatJS), or the whole file is not loaded.
plutil -lint checks .strings and .stringsdict files.use-escaping: true in l10n.yaml, quote literal braces: '{'.Source: ICU message formatting.
Plural categories come from the Unicode CLDR data built into Node.js (Intl.PluralRules), the
same data i18next and FormatJS select with at run time. English has one and other; Polish
and Russian add few and many; Arabic uses all six; Japanese only other. CLDR assigns
many to French, Spanish, Italian and Portuguese for large round numbers such as 1,000,000.
Missing plural form · warning
A plural in the translation lacks a category the locale's CLDR rules have, or the translation
does not vary by plural where the source does. Numbers in the missing category get the other
form, which is grammatically wrong in that language: in Polish, "5 plików" needs many, not the
other form "5 pliku". The finding lists the missing categories with example numbers.
.stringsdict, add the category key.few{…}.key_few; i18n-js: add a few entry.Sources: CLDR plural rules, CLDR language plural rules chart, Localizing strings that contain plurals, i18next plurals, Intl.PluralRules.
Plural without "other" · error
Every plural needs an other form: it is the form for any number no other category covers.
ICU requires it, and a .stringsdict rule without it has nothing to show for most numbers.
Duplicate key · error
The same key appears twice in one file. JSON.parse, Foundation and gen_l10n keep one of the
values without a warning; plutil -lint accepts a .strings file with a duplicate key, and so
does an Xcode build (see fixtures/uikit-strings). In JSON, a
duplicate object hides every key of the first copy, which GlobalLingo then also reports as
missing.
Unknown key · error
The code looks up a literal key that no catalog defines: NSLocalizedString("key", …),
String(localized: "key"), LocalizedStringResource("key"), Bundle.localizedString(forKey:)
(Apple, per table; tables the project does not define are left alone), t('key'),
<Trans i18nKey="key">, formatMessage({ id: 'key' }) and <FormattedMessage id="key">
(JavaScript; ns:key for i18next namespaces, plural base keys included). Keys built at run time
are not checked. Dart is not checked: a missing getter of the generated class does not compile.
SwiftUI text not in the String Catalog · warning
A SwiftUI view or modifier that takes a LocalizedStringKey (Text, Button, Label,
Toggle, TextField, .navigationTitle, …) gets a literal that the String Catalog of its
table does not have, so it shows the source text in every language. Build the target so Xcode
extracts it, translate it and commit the catalog; use Text(verbatim:) for text that must not be
translated. Literals with interpolation or % are left out, because Xcode turns them into format
keys.
Unused key · info
No source file mentions the key. Any string literal equal to the key counts as a use, a template
or interpolated string (t(`errors.${code}`), "status.\(name)") or a literal ending in .,
_ or : counts as a prefix of any key, Dart member access with the message's name counts, and
i18next $t(key) inside translations counts. String Catalogs are left to Xcode's stale marking
(STA001); InfoPlist, storyboard and App Shortcuts tables are not looked up by key in code. Code
can still build keys in ways no pattern sees, so the rule is info and does not fail a run unless
--fail-on info is set.
Hard-coded UI text · warning
Text shown to the user is a literal in the code: UIKit .text, .title, .placeholder,
setTitle, UIAlertController and UIAlertAction titles; Flutter Text('…') and text
parameters (hintText, labelText, tooltip, title, label, …); React Native <Text>
content, title, placeholder and accessibilityLabel props, header titles and Alert.alert.
Literals without letters, identifiers (ok_button, LOGIN), routes and URLs are left out, and so
are tests, stories, previews of generated code, and lines marked globallingo-ignore.
SwiftUI Text("…") is not hard-coded: the literal is a localization key (KEY002 checks it).
Turkish and Azerbaijani have a dotted and a dotless I in both cases: i ↔ İ and ı ↔ I.
Unicode's special casing gives them their own mappings, so case changes that use the locale
differ from those that do not:
| Case change | With the Turkish locale | With any other locale |
|---|---|---|
"title" to upper case |
TİTLE |
TITLE |
"TITLE" to lower case |
tıtle |
title |
"İ" to lower case |
i |
i̇ (i and U+0307) |
Functions that take the locale give the left column for Turkish: toLocaleUpperCase('tr') in
JavaScript, uppercased(with: Locale(identifier: "tr")) in Swift. Those that do not, such as
toUpperCase() in JavaScript and Dart and uppercased() in Swift, always give the right column.
Functions that use the device's locale (toLocaleUpperCase() without an argument,
localizedUppercase) give the left column on a Turkish device, whatever language the app shows.
Sources: SpecialCasing.txt, toLocaleUpperCase, toLocaleLowerCase, Dart String.toUpperCase. The Turkish I guide shows each case.
Locale-dependent case change on a key · warning
A lookup key goes through a case change that uses the device locale:
toLocaleLowerCase() or toLocaleUpperCase() without a locale (JavaScript), localizedLowercase,
localizedUppercase, lowercased(with: .current) (Swift), lowercaseStringWithLocale:[NSLocale currentLocale] (Objective-C). On a Turkish or Azerbaijani device "STATUS.IDLE" becomes
"status.ıdle" and the lookup misses, whatever languages the app ships. Use toLowerCase() or
lowercased(), which do not depend on the locale.
Dotted or dotless I in a key · warning
A key contains İ or ı. A case change of such a key does not give it back ("İ" lower-cases
to i plus a combining dot, "ı" upper-cases to I), so code that normalises keys misses it.
Rename the key to ASCII letters in every locale and in the code.
Translated text cased without the locale · warning
The app ships Turkish or Azerbaijani, and translated text is upper- or lower-cased right after the
lookup with a function that ignores the locale: t('city').toUpperCase(),
String(localized: …).uppercased(), .capitalized, or Dart's toUpperCase() on a message of the
generated localizations class. "istanbul" becomes "ISTANBUL" instead of "İSTANBUL".
uppercased(with: Locale(identifier: "tr")), or localizedUppercase when the text
is in the device's language.toUpperCase(); put the upper-case text in the ARB file
as its own message so the translator writes İ.toLocaleUpperCase(i18n.language).