Adds localization technote

And a missing translation
This commit is contained in:
Stuart Breckenridge
2023-01-08 06:35:22 +08:00
parent ce51182a92
commit b62dcf8d29
8 changed files with 269 additions and 9 deletions

70
Technotes/Localization.md Normal file
View File

@@ -0,0 +1,70 @@
# Localization
NetNewsWire is an Internationalized and Localized app.
## Internationalization
`Internationalized` means that, at code-level, we don't use locale-dependent strings. Instead, in code we use a key-based approach. Generally, this takes the form of:
`UIKit` or `AppKit` — using `NSLocalizedString`:
```swift
let messageFormat = NSLocalizedString("alert.title.open-articles-in-browser.%ld", comment: "Are you sure you want to open %ld articles in your browser?")
alert.messageText = String.localizedStringWithFormat(messageFormat, urlStrings.count)
```
or, in `SwiftUI` — using the `Text` struct:
```swift
Text("alert.title.remove-account.\(viewModel.accountToDelete?.nameForDisplay ?? "")", comment: "Are you sure you want to remove “%@“?")
```
### Key Format
All keys are lower cased and follow the `dot.separated` UI element name -> `hyphen-separated` string descriptor -> `dot.separated` variable list format, e.g.:
```
alert.title.open-articles-in-browser.%ld
button.title.close
```
### Comments
Whether using `NSLocalizedString` or `Text`, a `comment` must be provided. This will generally be a reference to the string in English. However, where a key contains multiple variables, the ordering of the variables must be specified in the comment.
## Localization
All of NetNewsWire's strings in code are localized in external resources — `.strings` or `.stringsdict`. Each target has its own `Localizable.strings` (and, where necessary, `.stringsdict`) files. All Storyboards are also localized.
### Adding New Strings
If you are developing a new feature that introduces new strings to the code base, follow these general guidelines:
- Check if there is an existing string in `Localizable.strings` that meets your needs. If there is, use that.
- If there isn't:
- Add your string in code following the key and comment rules above
- Run `Export Localizations`
- Open the `en.xcloc` file and provide the new translations.
- Save the `en.xcloc` file.
- Run `Import Localizations`
- Select `en.xcloc` and Import.
### Updating Existing Translations
Update the Development Language translation first:
- Run `Export Localizations`
- Open the `en.xcloc` file and provide the new translations.
- Save the `en.xcloc` file.
- Run `Import Localizations`
- Select `en.xcloc` and Import.
Then update other lanaguages:
- Run `Export Localizations`
- Open the `en-GB.xcloc` file and provide the new translations.
- Save the `en-GB.xcloc` file.
- Run `Import Localizations`
- Select `en-GB.xcloc` and Import.

View File

@@ -0,0 +1,39 @@
{
"configurations" : [
{
"id" : "3878F164-A79E-44EE-8DC3-3CFC413BCE62",
"name" : "English - US",
"options" : {
"areLocalizationScreenshotsEnabled" : true,
"language" : "en",
"region" : "US"
}
},
{
"id" : "4D5E7C9B-F2B7-4E6B-AF7E-FF172A8EC7BA",
"name" : "English - UK",
"options" : {
"areLocalizationScreenshotsEnabled" : true,
"language" : "en-GB",
"region" : "GB"
}
}
],
"defaultOptions" : {
"testTimeoutsEnabled" : true
},
"testTargets" : [
{
"skippedTests" : [
"NetNewsWireUITests_iOS",
"NetNewsWireUITests_iOSLaunchTests"
],
"target" : {
"containerPath" : "container:NetNewsWire.xcodeproj",
"identifier" : "DFB616C82965739D00A359AB",
"name" : "NetNewsWireUITests-iOS"
}
}
],
"version" : 1
}

View File

@@ -0,0 +1,71 @@
//
// LocalizationTests.swift
// NetNewsWireTests
//
// Created by Stuart Breckenridge on 04/01/2023.
// Copyright © 2023 Ranchero Software. All rights reserved.
//
import XCTest
final class LocalizationTests_iOSTest: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testAppRunThrough() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
_ = addUIInterruptionMonitor(withDescription: "Handle Notifications Alert") { element in
if element.buttons["Allow"].exists {
element.buttons["Allow"].tap()
return true
}
return false
}
app.toolbars["Toolbar"].buttons["Settings"].tap()
let collectionViewsQuery = app.collectionViews
collectionViewsQuery/*@START_MENU_TOKEN@*/.buttons["Manage Accounts"]/*[[".cells.buttons[\"Manage Accounts\"]",".buttons[\"Manage Accounts\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
app.navigationBars["Manage Accounts"]/*@START_MENU_TOKEN@*/.buttons["Add"]/*[[".otherElements[\"Add\"].buttons[\"Add\"]",".buttons[\"Add\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
app.navigationBars["Add Account"]/*@START_MENU_TOKEN@*/.buttons["Cancel"]/*[[".otherElements[\"Cancel\"].buttons[\"Cancel\"]",".buttons[\"Cancel\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
app.navigationBars["Manage Accounts"].buttons["Settings"].tap()
collectionViewsQuery/*@START_MENU_TOKEN@*/.buttons["Manage Extensions"]/*[[".cells.buttons[\"Manage Extensions\"]",".buttons[\"Manage Extensions\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
app.navigationBars["Manage Extensions"]/*@START_MENU_TOKEN@*/.buttons["Add"]/*[[".otherElements[\"Add\"].buttons[\"Add\"]",".buttons[\"Add\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
app.navigationBars["Add Extensions"]/*@START_MENU_TOKEN@*/.buttons["Cancel"]/*[[".otherElements[\"Cancel\"].buttons[\"Cancel\"]",".buttons[\"Cancel\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
app.navigationBars["Manage Extensions"].buttons["Settings"].tap()
collectionViewsQuery/*@START_MENU_TOKEN@*/.buttons["Import Subscriptions"]/*[[".cells.buttons[\"Import Subscriptions\"]",".buttons[\"Import Subscriptions\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
app/*@START_MENU_TOKEN@*/.scrollViews/*[[".otherElements[\"Choose an account to receive the imported feeds and folders\"].scrollViews",".scrollViews"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.otherElements.buttons["Cancel"].tap()
collectionViewsQuery/*@START_MENU_TOKEN@*/.buttons["Export Subscriptions"]/*[[".cells.buttons[\"Export Subscriptions\"]",".buttons[\"Export Subscriptions\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
app/*@START_MENU_TOKEN@*/.scrollViews/*[[".otherElements[\"Choose an account with the subscriptions to export\"].scrollViews",".scrollViews"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.otherElements.buttons["Cancel"].tap()
let displayBehaviorsButton = collectionViewsQuery.buttons["button.title.display-and-behaviors"]
displayBehaviorsButton.tap()
app.navigationBars.buttons["Settings"].tap()
collectionViewsQuery/*@START_MENU_TOKEN@*/.buttons["New Article Notifications"]/*[[".cells.buttons[\"New Article Notifications\"]",".buttons[\"New Article Notifications\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
app.navigationBars["New Article Notifications"].buttons["Settings"].tap()
displayBehaviorsButton.swipeUp()
collectionViewsQuery/*@START_MENU_TOKEN@*/.buttons["About"]/*[[".cells.buttons[\"About\"]",".buttons[\"About\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
app.navigationBars["About"].buttons["Settings"].tap()
}
private func addScreenShot(_ name: String, app: XCUIApplication) {
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
}
}

View File

@@ -0,0 +1,42 @@
//
// NetNewsWireUITests_iOS.swift
// NetNewsWireUITests-iOS
//
// Created by Stuart Breckenridge on 04/01/2023.
// Copyright © 2023 Ranchero Software. All rights reserved.
//
import XCTest
final class NetNewsWireUITests_iOS: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
}

View File

@@ -0,0 +1,33 @@
//
// NetNewsWireUITests_iOSLaunchTests.swift
// NetNewsWireUITests-iOS
//
// Created by Stuart Breckenridge on 04/01/2023.
// Copyright © 2023 Ranchero Software. All rights reserved.
//
import XCTest
final class NetNewsWireUITests_iOSLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}

View File

@@ -263,9 +263,8 @@ private extension MasterTimelineTableViewCell {
}
func updateAccessiblityLabel() {
#warning("This needs to be localized.")
let starredStatus = cellData.starred ? "\(NSLocalizedString("Starred", comment: "Starred article for accessibility")), " : ""
let unreadStatus = cellData.read ? "" : "\(NSLocalizedString("Unread", comment: "Unread")), "
let starredStatus = cellData.starred ? "\(NSLocalizedString("label.text.starred", comment: "Starred")), " : ""
let unreadStatus = cellData.read ? "" : "\(NSLocalizedString("label.text.unread", comment: "Unread")), "
let label = starredStatus + unreadStatus + "\(cellData.feedName), \(cellData.title), \(cellData.summary), \(cellData.dateString)"
accessibilityLabel = label
}

View File

@@ -745,6 +745,9 @@
/* Small */
"label.text.small" = "Small";
/* Starred */
"label.text.starred" = "Starred";
/* Thanks */
"label.text.thanks" = "Thanks";
@@ -760,6 +763,9 @@
/* Timeline */
"label.text.timeline" = "Timeline";
/* Unread */
"label.text.unread" = "Unread";
/* Relative time that the account was last refreshed. The variable is a named relative time. Example: Updated 8 minutes ago */
"label.text.updatedat.%@" = "Updated %@";
@@ -857,9 +863,6 @@
/* Smart Feeds group title */
"smartfeeds.title" = "Smart Feeds";
/* Starred article for accessibility */
"Starred" = "Starred";
/* Email Address */
"textfield.placeholder.email-address" = "Email Address";
@@ -911,6 +914,3 @@
/* Unread label for accessiblity */
"unread" = "unread";
/* Unread */
"Unread" = "Unread";

View File

@@ -730,6 +730,9 @@
/* Small */
"label.text.small" = "Small";
/* Starred */
"label.text.starred" = "Starred";
/* Thanks */
"label.text.thanks" = "Thanks";
@@ -745,6 +748,9 @@
/* Timeline */
"label.text.timeline" = "Timeline";
/* Unread */
"label.text.unread" = "Unread";
/* Relative time that the account was last refreshed. The variable is a named relative time. Example: Updated 8 minutes ago */
"label.text.updatedat.%@" = "Updated %@";