As you'd expect, given Wikipedia's mission and core values, making the iOS app accessible & usable in as many languages as possible is very important to us. This document is a quick overview of how we localize strings as well as the app itself.
Localized strings which are used by the app are all kept in Localizable.strings. We do not use localized string entries generated by Interface Builder.
Please don't copy the code formatting on this page into source. It's formatted for readability on the web, not a source file.
TL;DR: WMFLocalizedStringWithDefaultValue
in Obj-C, WMFLocalizedString
in Swift.
Use keys that match this convention: "places-filter-saved-articles-count"
- "feature-name-info-about-the-string"
. Do not change the keys for localized strings. Unless absolutely necessary, keys should remain the same to prevent complications when syncing with TWN.
ALWAYS USE ORDERED STRING FORMAT SPECIFIERS even if there's only one format specifier - For example: %1$@
instead of %@
, %1$d
instead of %d
, etc. For strings with multiple specifiers, increment the number:
WMFLocalizedString(
"places-search-articles-that-match",
value:"%1$@ matching “%2$@”",
comment:"A search suggestion for filtering the articles in the area by the search string. %1$@ is replaced by the filter ('Top articles' or 'Saved articles'). %2$@ is replaced with the search string"
)
WMFLocalizedStringWithDefaultValue
matches the signature of NSLocalizedStringWithDefaultValue
with one exception: the second parameter is an optional siteURL
which will cause the returned localization to be in the language of the Wikipedia site at siteURL
. For example, if siteURL
's host is fr.wikipedia.org
, the returned string will be in French, even if the user's default language is English. This method matches the naming convention and number of parameters of NSLocalizedStringWithDefaultValue
so that we can use the tools provided with Xcode for extracting string values from code automatically. Bundle should always be nil.
To get a string localized to the user's system default locale:
WMFLocalizedStringWithDefaultValue(
@"article-about-title",
nil,
nil,
@"About this article",
@"The text that is displayed before the 'about' section at the bottom of an article"
);
To get a string localized to the language of the Wikipedia site indicated by siteURL
:
WMFLocalizedStringWithDefaultValue(
@"article-about-title",
siteURL,
nil,
@"About this article",
@"The text that is displayed before the 'about' section at the bottom of an article"
);
Plural string. Note the use of localizedStringWithFormat
instead of stringWithFormat
:
[NSString localizedStringWithFormat:
WMFLocalizedStringWithDefaultValue(
@"places-filter-saved-articles-count",
nil,
nil,
@"{{PLURAL:%1$d|0=No saved places|%1$d place|%1$d places}} found",
@"Describes how many saved articles are found in the saved articles filter - %1$d is replaced with the number of articles"
),
savedCount
)
In Swift, WMFLocalizedString
matches the signature of NSLocalizedString
with one exception - the second parameter is an optional siteURL
which will cause the returned localization to be in the language of the Wikipedia site at siteURL
. For example, if siteURL
's host is fr.wikipedia.org
, the returned string will be in French, even if the user's default language is English. This method matches the naming convention and number of parameters of NSLocalizedString
so that we can use the tools provided with Xcode for extracting string values from code automatically. You should always omit bundle:
.
To get a string localized to the user's system default locale:
WMFLocalizedString(
"places-filter-saved-articles",
value:"Saved articles",
comment:"Title of places search filter that searches saved articles"
)
To get a string localized to the language of the Wikipedia site indicated by siteURL
:
WMFLocalizedString(
"places-filter-saved-articles",
siteURL:siteURL,
value:"Saved articles",
comment:"Title of places search filter that searches saved articles"
)
Plural string. Note the use of localizedStringWithFormat
instead of stringWithFormat
:
String.localizedStringWithFormat(
WMFLocalizedString(
"places-filter-saved-articles-count",
value:"{{PLURAL:%1$d|0=No saved places|%1$d place|%1$d places}} found",
comment:"Describes how many saved articles are found in the saved articles filter - %1$d is replaced with the number of articles"
),
savedCount
)
Ensure the last variant is the "other" or "default" variant - in these cases it's %1$d places. Ensure the format specifier appears in the "other" variant. For example, %1$d {{PLURAL:%1$d|place|places}}
is invalid, {{PLURAL:%1$d|one place|%1$d places}}
is valid. Plural strings can only contain one format specifier and only one plural per string at the moment (can be fixed by updating the localization script localization.swift).
Without "zero" value:
{{PLURAL:%1$d|%1$d place|%1$d places}}
With "zero" value:
{{PLURAL:%1$d|0=You have no saved places|%1$d place|%1$d places}}
We don't support arbitrary numerals, only 0=
. For example, {{PLURAL:%1$d|12=a dozen places|one place|%1$d places}}
is invalid.
- Developer adds localized strings & comments to source using the methods described above.
- Developer builds & runs the app. The app has two run script build phases that automatically extracts the strings from source and adds them to the appropriate localization bundles (for both the app
Wikipedia/iOS Native Localizations
& TWNWikipedia/Localizations
) - A script maintained by Translatewiki pulls the repo, reads the new strings from
Wikipedia/Localizations
, and adds them to Translatewiki - Volunteer translators translate the strings
- The same script maintained by Translatewiki adds the new translations to the
Wikipedia/Localizations
folder in the TWN format and pushes the changes to thetwn
branch. - Our Jenkins
localization
job notices the changes to the twn branch, runsscripts/localization import
which adds the iOS-formatted strings toWikipedia/iOS Native Localizations
and submits a PR.
scripts/localization_extract
extracts those strings from source, generates the en
translation inside of Wikipedia/iOS Native Localizations
.
scripts/localization export
creates translatewiki-formatted qqq
(comments only) and en
(translations) inside of Wikipedia/Localizations
for Translatewiki to read.
Translatewiki's script reads the Wikipedia/Localizations
qqq
and en
files, imports them to the wiki, and writes updated translations for other languages to Wikipedia/Localizations
scripts/localization import
reads localizations from Wikipedia/Localizations
and converts them into the iOS native format for Wikipedia/iOS Native Localizations
Inside the main project (Wikipedia.xcodeproj
), there's a localization
target that is a Mac command line tool. You can edit the swift files used by this target (scripts/localization.swift
, localization/main.swift
) to update the localization script. Once you make changes, you can build and run the localization target to re-run localizations and verify the output. Once you're done making changes, you need to archive the localization
target and copy the binary to scripts/localizations
where it will be run with every build.
Some important things to test across different locales (and operating systems):
- View layout in LTR & RTL environments
- custom
NSDateFormatter
- Data models for horizontal navigation which need to be reversed when app is RTL (e.g. image gallery data sources)
Text overflow is also an important consideration when designing and implementing views, but doesn't require exhaustive locale testing. Typically, it's sufficient to pass short, medium, and long strings to the test subject and verify proper wrapping, truncating, and/or layout behavior. See WMFArticleListCellVisualTests
for an example.
We run a certain set of tests across multiple operating systems and locales to verify business logic, and especially views, exhibit proper conditional behavior & appearance. From a project setup standpoint, this involves:
- Running LTR tests on the main scheme on iOS 8 & 9 simulators
- Running RTL tests in a separate, Wikipedia RTL scheme on iOS 8 & 9 simulators
The RTL locale & writing direction are forced in the scheme using launch arguments as described in the Testing Right-to-Left Layouts section of Apple's "Internationalization and Localization Guide."
Ideally, the code should be factored in such a way that the relevant inputs (i.e. OS version and/or layout direction) can be passed explicitly during tests. For example, given a method that returns a different value based on a layout direction:
// Method invoked in unit tests w/ different layout directions
- (BOOL)methodDoingSomethingForWritingDirection:(UIUserInterfaceLayoutDirection)layoutDirection;
// Method invoked in application code, which passes the `[[UIApplication sharedApplication] userInterfaceLayoutDirection]`
// to the first argument of the first method signature.
- (BOOL)methodDoingSomethingForApplicationWritingDirection;
In other cases where this isn't feasible, you'll need to add your test class to the WikipediaRTL scheme so that the application itself is in RTL. Also, you'll need to write assertions based on the writing direction and/or OS at runtime (see WMFGalleryDataSourceTests
for an example). NSDate+WMFPOTDTitleTests
are another example that rely on the application state, and verify that the date is not affected by the current locale—which NSDateFormatter
implicitly uses when computing strings from dates.
Visual tests can be incredibly useful when verifying LTR & RTL responsiveness across multiple OS versions. Write you visual test as you normally would, ensure it's added to the WikipediaRTL scheme, and use the WMFSnapshotVerifyViewForOSAndWritingDirection
convenience macro to record & compare your view with a reference image dedicated to a specific OS version and writing direction. See WMFTextualSaveButtonLayoutVisualTests
for an example.