mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Adds localization technote
And a missing translation
This commit is contained in:
70
Technotes/Localization.md
Normal file
70
Technotes/Localization.md
Normal 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.
|
||||
|
||||
39
Tests/NetNewsWireUITests-iOS/LocalizationTestPlan.xctestplan
Normal file
39
Tests/NetNewsWireUITests-iOS/LocalizationTestPlan.xctestplan
Normal 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
|
||||
}
|
||||
71
Tests/NetNewsWireUITests-iOS/LocalizationTests.swift
Normal file
71
Tests/NetNewsWireUITests-iOS/LocalizationTests.swift
Normal 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 it’s 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)
|
||||
}
|
||||
|
||||
}
|
||||
42
Tests/NetNewsWireUITests-iOS/NetNewsWireUITests_iOS.swift
Normal file
42
Tests/NetNewsWireUITests-iOS/NetNewsWireUITests_iOS.swift
Normal 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 it’s 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 %@";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user