Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

211 lines
17 KiB
Markdown

# Localization and Internationalization
As you'd expect, given [Wikipedia's mission](https://wikimediafoundation.org/about/mission/) and [core values](https://wikimediafoundation.org/about/jobs/our-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
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.
### CommonStrings.swift
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.
### Adding a new localized string
**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:
```swift
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"
)
````
#### Obj-C
`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:
```objc
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`:
```objc
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`:
```objc
[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
)
```
#### Swift
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:
```swift
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`:
```swift
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`:
```swift
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
)
```
#### Plural syntax
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](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/StringsdictFileFormat/StringsdictFileFormat.html#//apple_ref/doc/uid/10000171i-CH16-SW1).
More information about MediaWiki plural support can be found [on Translatewiki's page for plural handling](https://translatewiki.net/wiki/Plural#Plural_in_MediaWiki).
### Updating an existing string
**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 to `Take 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 to `Take 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).
### Translation workflow
1. Developer adds localized strings & comments to source using the methods described above.
2. 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` & TWN `Wikipedia/Localizations`)
3. A script maintained by Translatewiki pulls the repo, reads the new strings from `Wikipedia/Localizations`, and adds them to Translatewiki
4. On Translatewiki, volunteer translators translate the string.
5. The same script maintained by Translatewiki adds the new translations to the `Wikipedia/Localizations` folder in the TWN format, pushes the changes to the `twn` branch, and opens a pull request.
6. GitHub notices the pull request, and via a GitHub Action (`.github/workflows/localization-update.yaml`) runs `scripts/localization import` which adds the iOS-formatted strings to `Wikipedia/iOS Native Localizations`.
7. `TWNStringsTests.m` is run, ensuring that the strings are the format that is expected.
8. An iOS Engineer approves the pull request.
### Script specifics
`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`
### Updating the localization script
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.
### Testing the app
#### Indications for international testing
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`](../WikipediaUnitTests/Code/WMFArticleListCellVisualTests.m) for an example.
#### Internationalization testing strategies
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](https://developer.apple.com/library/ios/documentation/MacOSX/Conceptual/BPInternational/TestingYourInternationalApp/TestingYourInternationalApp.html) section of Apple's "Internationalization and Localization Guide."
##### Unit tests
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:
``` objc
// 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`](../WikipediaUnitTests/Code/WMFGalleryDataSourceTests.m#L36) for an example). [`NSDate+WMFPOTDTitleTests`](../WikipediaUnitTests/Code/NSDate+WMFPOTDTitleTests.m) 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
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`](../WikipediaUnitTests/Code/WMFTextualSaveButtonLayoutVisualTests.m) for an example.
### Troubleshooting
#### Fixing failing tests
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](https://translatewiki.net/w/i.php?title=Special:Translate&group=out-wikimedia-mobile-wikipedia-ios-0-all&filter=&action=translate).
#### Missing translations
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.
1. 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.
2. In the languages disclosure menu, choose the language whose localizations you wish to add.
3. 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).
4. Tap finish. Commit the changes with a message like "Added {Language Name} language to project for localizations"."
5. 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.
6. Commit your changes from running the import script in the previous step.
7. Push your changes up to the TranslateWiki PR.
8. Confirm unit tests pass, then approve and merge.