Skip to content

Commit

Permalink
Add backwards compatible initialisers for Text and `LocalizedString…
Browse files Browse the repository at this point in the history
…sKey` (#82)

* Generate Text.init(localizable:)

* Generate LocalizedStringKey.init(localizable:)

* Generate with correct access levels and add static method accesors

* Add better documentation to generated code

* Update docc documentation

* Update README.md

* Fix warnings in Package.swift

* Always use LocalizedStringResource when possible
  • Loading branch information
liamnichols committed May 27, 2024
1 parent 616763c commit cbe577e
Show file tree
Hide file tree
Showing 28 changed files with 1,677 additions and 72 deletions.
14 changes: 12 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ let package = Package(
.target(name: "StringExtractor"),
.target(name: "StringGenerator"),
.target(name: "StringResource"),
.target(name: "StringValidator")
.target(name: "StringValidator"),
.target(name: "Version")
]
),

Expand All @@ -57,7 +58,8 @@ let package = Package(
.target(name: "SwiftIdentifier"),
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax")
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
.target(name: "Version")
]
),

Expand All @@ -73,6 +75,10 @@ let package = Package(
name: "SwiftIdentifier"
),

.target(
name: "Version"
),

.target(
name: "StringExtractor",
dependencies: [
Expand Down Expand Up @@ -108,6 +114,10 @@ let package = Package(
dependencies: [
.target(name: "XCStringsToolPlugin")
],
resources: [
.process("FeatureOne.xcstrings"),
.process("Localizable.xcstrings")
],
swiftSettings: [
.define("XCSTRINGS_TOOL_ACCESS_LEVEL_PUBLIC")
]
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ struct ContentView: View {
ItemView(item)
}
} footer: {
Text(.localizable.footerText(items.count))
Text(.localizable(.footerText(items.count)))
}
}
.navigationTitle(Text(.localizable.contentViewTitle))
.navigationTitle(.localizable(.contentViewTitle))
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ How to use the generated source code effectively in your project.

## Overview

For each Strings Catalog in your target, XCStrings Tool will produce a self-contained source file that consists primarily of a struct that is named after the Strings Catalog. The generated code extends two main Foundation APIs

- [Resolving a String](#Resolving-a-String)
- [Using LocalizedStringResource](#Using-LocalizedStringResource)
For each Strings Catalog in your target, XCStrings Tool will produce a self-contained source file named after the Strings Catalog. Within the source file contains a struct with a static method or property for each localization and a series of extensions that allow you to create instances of `String`, `LocalizedStringResource`, `LocalizedStringKey` or `Text`.

### Resolving a String

Expand All @@ -25,49 +22,34 @@ Lets break down the generated code that makes this example possible:
2. `String.Localizable.headingTitle` - A static variable that describes the `HeadingTitle` key from the strings catalog.
3. `String.init(localizable:locale:)` - An initializer that can be used to resolve the localized value of the phrase.

The source code used in this example is compatible across all OS versions as it uses `Bundle.localizedString(forKey:value:table:)` internally.
The source code used in this example is compatible across all OS versions as it uses [`Bundle.localizedString(forKey:value:table:)`](https://developer.apple.com/documentation/foundation/bundle/1417694-localizedstring) internally.

### Using LocalizedStringResource
### Converting to LocalizedStringResource

Stating in iOS 16/macOS 13/watchOS 9/tvOS 16, a new `LocalizedStringResource` type was introduced to help defer the resolution of localized text in order to support features such as App Intents and the SwiftUI environment.

For example, if the SwiftUI environment overrides the `locale`, it is expected that Text and other localized resources are resolved using that `locale` rather than the app or device language.
For example, if the SwiftUI environment overrides the `locale`, it is expected that `Text` and other localized resources are resolved using that `locale` rather than the app or device language.

XCStrings Tool will also generate the following:
You can create a `LocalizedStringResource` like so:

```swift
let resource = LocalizedStringResource.localizable.headingTitle
var resource = LocalizedStringResource(localizable: .headingTitle)
resource.locale = Locale(identifier: "fr")
let frenchValue = String(localized: resource)
```

If your project needs to override the language, you can leverage the `LocalizedStringResource` like in the following examples:
### Working with SwiftUI

#### SwiftUI

```swift
Text(.localizable.headingTitle)
.environment(\.locale, Locale(identifier: "fr"))
```
While `LocalizedStringResource` is supported on some SwiftUI types, it is not available everywhere and it cannot work on older operating system versions. As a result, your generated code will contain additionl methods to improve usage.

> Not all SwiftUI types have been updated to accept `LocalizedStringResource`.
>
> In some instances, you might need to wrap the `LocalizedStringResource` in `Text` or `LocalizedStringKey` as per the following examples
>
> ```swift
> // Text
> Button(action: { rows.append("New Row") }, label: {
> Text(.localizable.addRow)
> })
>
> // LocalizedStringKey
> Button("\(.localizable.addRow)") { rows.append("New Row") }
> ```
#### Foundation
This consists of `Text.init(localizable:)`, `LocalizedStringKey.init(localizable:)` as well as the `LocalizedStringKey.localizable(_:)` and `LocalizedStringResource.localizable(_:)` convenience methods.

```swift
var resource = LocalizedStringResource.localizable.headingTitle
resource.locale = Locale(identifier: "fr")
let frenchValue = String(localized: resource)
var body: some View {
List {
Text(localizable: .listContent)
}
.navigationTitle(.localizable(.headingTitle))
.environment(\.locale, Locale(identifier: "fr"))
}
```


41 changes: 41 additions & 0 deletions Sources/StringGenerator/Extensions/IfExprSyntax.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import SwiftSyntax
import SwiftSyntaxBuilder

extension IfExprSyntax {
init(
availability: AvailabilityArgumentListSyntax,
body: CodeBlockSyntax,
elseBody: CodeBlockSyntax? = nil
) {
self.init(
conditions: ConditionElementListSyntax {
AvailabilityConditionSyntax(availabilityKeyword: .poundAvailableToken(), availabilityArguments: availability)
},
body: body,
elseKeyword: elseBody.flatMap { _ in .keyword(.else) },
elseBody: elseBody.flatMap { .codeBlock($0) }
)
}

init(
availability: AvailabilityArgumentListSyntax,
@CodeBlockItemListBuilder bodyBuilder: () -> CodeBlockItemListSyntax
) {
self.init(
availability: availability,
body: CodeBlockSyntax(statements: bodyBuilder())
)
}

init(
availability: AvailabilityArgumentListSyntax,
@CodeBlockItemListBuilder bodyBuilder: () -> CodeBlockItemListSyntax,
@CodeBlockItemListBuilder elseBodyBuilder: () -> CodeBlockItemListSyntax
) {
self.init(
availability: availability,
body: CodeBlockSyntax(statements: bodyBuilder()),
elseBody: CodeBlockSyntax(statements: elseBodyBuilder())
)
}
}
1 change: 1 addition & 0 deletions Sources/StringGenerator/Extensions/TokenSyntax+Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ extension TokenSyntax {
case Argument
case CVarArg
case LocalizedStringKey
case Text
}

static func type(_ value: MetaType) -> TokenSyntax {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import StringResource
import SwiftSyntax
import Version

extension SourceFile.StringExtension {
struct StringsTableStruct {
Expand Down Expand Up @@ -66,13 +67,52 @@ extension SourceFile.StringExtension {

var headerDocumentation: String {
"""
Constant values for the \(sourceFile.tableName) Strings Catalog
A type that represents localized strings from the ‘\(sourceFile.tableName)
strings table.
Do not initialize instances of this type yourself, instead use one of the static
methods or properties that have been generated automatically.
## Usage
### Foundation
In Foundation, you can resolve the localized string using the system language
with the `String`.``Swift/String/init(\(sourceFile.tableVariableIdentifier):locale:)``
intializer:
```swift
// Accessing the localized value directly
let value = String(\(sourceFile.tableVariableIdentifier): .\(example.name))
value // \"\(example.value)\"
```
Starting in iOS 16/macOS 13/tvOS 16/watchOS 9, `LocalizedStringResource` can also
be used:
```swift
var resource = LocalizedStringResource(\(sourceFile.tableVariableIdentifier): .\(example.name))
resource.locale = Locale(identifier: "fr") // customise language
let value = String(localized: resource) // defer lookup
```
### SwiftUI
In SwiftUI, it is recommended to use `Text`.``SwiftUI/Text/init(\(sourceFile.tableVariableIdentifier):)``
or `LocalizedStringKey`.``SwiftUI/LocalizedStringKey/\(sourceFile.tableVariableIdentifier)(_:)``
in order for localized values to be resolved within the SwiftUI environment:
```swift
var body: some View {
List {
Text(\(sourceFile.tableVariableIdentifier): .listContent)
}
.navigationTitle(.\(sourceFile.tableVariableIdentifier)(.navigationTitle))
.environment(\\.locale, Locale(identifier: "fr"))
}
```
- SeeAlso: [XCStrings Tool Documentation - Using the generated source code](https://swiftpackageindex.com/liamnichols/xcstrings-tool/\(version)/documentation/documentation/using-the-generated-source-code)
"""
}
}
Expand Down
30 changes: 30 additions & 0 deletions Sources/StringGenerator/Snippets/IfCanImportSnippet.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import SwiftSyntax
import SwiftSyntaxBuilder

struct IfCanImportSnippet {
let module: TokenSyntax.Module
let items: CodeBlockItemListSyntax
}

extension IfCanImportSnippet {
init(
module: TokenSyntax.Module,
@CodeBlockItemListBuilder itemsBuilder: () -> CodeBlockItemListSyntax
) {
self.init(module: module, items: itemsBuilder())
}
}

extension IfCanImportSnippet: Snippet {
var syntax: some DeclSyntaxProtocol {
IfConfigDeclSyntax(
clauses: IfConfigClauseListSyntax {
IfConfigClauseSyntax(
poundKeyword: .poundIfToken(),
condition: CanImportExprSyntax(importPath: .module(module)),
elements: .statements(items)
)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct LocalizedStringResourceInitializerSnippet {
extension LocalizedStringResourceInitializerSnippet: Snippet {
var syntax: some DeclSyntaxProtocol {
InitializerDeclSyntax(
modifiers: modifiers,
signature: FunctionSignatureSyntax(
parameterClause: FunctionParameterClauseSyntax(
leftParen: .leftParenToken(),
Expand Down Expand Up @@ -59,4 +60,9 @@ extension LocalizedStringResourceInitializerSnippet: Snippet {
.multiline()
}
}

@DeclModifierListBuilder
var modifiers: DeclModifierListSyntax {
DeclModifierSyntax(name: stringsTable.accessLevel.token)
}
}
39 changes: 38 additions & 1 deletion Sources/StringGenerator/Snippets/SourceFileSnippet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,46 @@ struct SourceFileSnippet: Snippet {
LocalizedStringResourceInitializerSnippet(
stringsTable: sourceFile.stringExtension.stringsTableStruct
)

StringsTableConversionStaticMethodSnippet(
stringsTable: sourceFile.stringExtension.stringsTableStruct,
returnType: .type(.LocalizedStringResource)
)
}

IfCanImportSnippet(module: .SwiftUI) {
ImportSnippet(module: .SwiftUI)
.syntax
.with(\.trailingTrivia, .newlines(2))

ExtensionSnippet(
availability: .wwdc2019,
extending: .type(.Text)
) {
TextInitializerSnippet(
stringsTable: sourceFile.stringExtension.stringsTableStruct
)
}
.syntax
.with(\.trailingTrivia, .newlines(2))

ExtensionSnippet(
availability: .wwdc2019,
extending: .type(.LocalizedStringKey)
) {
LocalizedStringKeyInitializerSnippet(
stringsTable: sourceFile.stringExtension.stringsTableStruct
)

StringsTableConversionStaticMethodSnippet(
stringsTable: sourceFile.stringExtension.stringsTableStruct,
returnType: .type(.LocalizedStringKey)
)

LocalizedStringKeyOverrideKeySnippet()
}
}
}
.spacingStatements()
.with(\.trailingTrivia, .newline)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,17 @@ struct StringStringsTableStructSnippet: Snippet {
StructDeclSyntax(
leadingTrivia: leadingTrivia,
modifiers: modifiers,
name: stringsTable.type
) {
name: stringsTable.type,
memberBlock: memberBlock
)
}

var leadingTrivia: Trivia? {
Trivia(docComment: stringsTable.headerDocumentation)
}

var memberBlock: MemberBlockSyntax {
MemberBlockSyntax {
// enum BundleDescription { ... }
StringStringsTableBundleDescriptionEnumSnippet(bundleDescription: stringsTable.bundleDescriptionEnum)
.syntax
Expand Down Expand Up @@ -80,10 +89,6 @@ struct StringStringsTableStructSnippet: Snippet {
}
}

var leadingTrivia: Trivia? {
Trivia(docComment: stringsTable.headerDocumentation)
}

@DeclModifierListBuilder
var modifiers: DeclModifierListSyntax {
DeclModifierSyntax(name: stringsTable.accessLevel.token)
Expand Down
Loading

0 comments on commit cbe577e

Please sign in to comment.