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.
Strings that will be used in multiple places should be put in CommonStrings.swift
. If re-using an existing string translation, move it into CommonStrings.swift
. The same key should not be hardcoded in multiple places, as that can lead to problems.
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. This should be put in any source file (or CommonStrings.swift
, for frequently used strings), and everything else will happen automatically. Scripts will automatically create proper entries in qqq.json
and related files.
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.
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}}
iOS doesn't support arbitrary numerals, only 0=
. For example, the 12=
translation in {{PLURAL:%1$d|12=a dozen places|one place|%1$d places}}
can't be utilized on iOS. We allow users on Translatewiki to enter arbitrary numeral translations should there ever be a way to support it on iOS.
For some languages, the singular form only applies to n=1
. In these languages, we can map the Translatewiki's 1=
translations to the one
key on iOS. For example, if n is 'years ago' and the translation is 1=Last year
, this works for languages where one
is only ever used for n=1
. For other languages, like Russian, the value for the one
translation is used for certain numbers ending in 1. If we mapped the Russian Translatewiki value for 1=
to iOS's one
, it would use the Russian equivalent of Last year
for n=1,11,21,31,...
years ago.
More information about iOS plural support can be found in Apple's documentation for the stringsdict file format.
More information about MediaWiki plural support can be found on Translatewiki's page for plural handling.
Small changes: Do not change the key, just update the value
field in WMFLocalizedString
. Via the scripts, Translatewiki will automatically mark the translation string for review by translators. In the meantime, the old translation will continue to be used. (In our files from Translatewiki, fuzzy
indicates that the translation needs review.)
Large changes: Update the key, as well as the value. This will create a new translation. Until it is translated for a given langauge, the English string will be shown.
To decide whether it is a small or large change, consider this: Until a translator reviews the string for a given language, what is a better sitaution for the user? Continuing to show the old translation, or showing English? If "old translation", it's a small change and you shouldn't touch the key. If "showing English", it's a large change and you should use a new key.
- Example 1:
Take a left turn, then walk forward for a while
is getting updated toTake a left turn, then walk forward 10 yards
. This is a small change, because it's better for languages to show the old translation rather than the new English one. The new one has better fidelity, but the old one is still correct. - Example 2:
Take a left turn, then walk forward for a while
is getting updated toTake a right turn, then walk forward for a while
. This is a large change, because it's better to show English rather than the old translation. The update changes the meaning of the string, and if we don't change the key the old translations will continue be shown (until a translator reviews them).
- 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 - On Translatewiki, volunteer translators translate the string.
- The same script maintained by Translatewiki adds the new translations to the
Wikipedia/Localizations
folder in the TWN format, pushes the changes to thetwn
branch, and opens a pull request. - GitHub notices the pull request, and via a GitHub Action (
.github/workflows/localization-update.yaml
) runsscripts/localization import
which adds the iOS-formatted strings toWikipedia/iOS Native Localizations
. TWNStringsTests.m
is run, ensuring that the strings are the format that is expected.- An iOS Engineer approves the pull request.
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, there's a localization
target that is a Mac command line tool. You can edit the swift files used by this target (inside Command Line Tools/Update Localizations/
) to update the localization script. Once you make changes, you can build and run the localization target through the Update Localizations
scheme to re-run localizations and verify the output. Once you're done making changes, create an executable by selecting the "Update Localizations" scheme > My Mac, then select Product > Archive from the Xcode menu. Once the Xcode Organizer window appears, choose your new archive, then select "Distribute Content". Choose "Built Products" on the first screen, then select a location on your machine to save the new product. Move the new localizations binary from within the product sudirectories (example: Desktop/Update Localizations yyyy-mm-dd hh-mm-ss/Products/usr/local/bin/localizations into the scripts/localizations
location in the repo. Then commit your updated script binary changes to the repo.
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.
When a test fails, it should provide useful output as to which languages and strings are incorrect. A common situation is a translator trying to give options (singular/plural, for example) for a string that isn't designed to take one. In some cases, the translation string in the app should be updated to allow for the options that the translator tried to use, as it will allow for better translations in that language. In other cases, the translation string should be fixed on Translatewiki.
Oftentimes iOS engineers need to do a one-time extra step to add a localization language to the project before our importing script will pick up the TranslateWiki changes. If you notice a PR from TranslateWiki containing a new .strings/.stringsdict files within the Wikipedia/Localizations subdirectory, but they are missing in Wikipedia/iOS Native Localization subdirectory, then we need to perform this step.
- In the project navigator, select the Wikipedia Project item to open the project settings, then select the Wikipedia project (not the target) in the projects targets list panel. Choose the Info tab. You will see a section called Localizations. Scroll down to the bottom and tap the + button.
- In the languages disclosure menu, choose the language whose localizations you wish to add.
- You will see a prompt that says "Choose files and reference language to create {language} localization". Select the first 3 files (InfoPlist.strings, Localizable.strings, and Localizable.stringsdict) and uncheck the rest. These should indicate that they are located under the Wikipedia > Localizations subdirectory. Leave the Reference Language column as-is (English).
- Tap finish. Commit the changes with a message like "Added {Language Name} language to project for localizations"."
- Now choose the "Update Localizations" scheme and run it to execute the import script. This will import the new TranslateWiki translations into the proper strings files within the Wikipedia/iOS Native Localizations subdirectory.
- Commit your changes from running the import script in the previous step.
- Push your changes up to the TranslateWiki PR.
- Confirm unit tests pass, then approve and merge.