Skip to content

Commit 8efe62a

Browse files
authored
Release 0.3.0 > main
2 parents 83840a9 + b5e3bde commit 8efe62a

18 files changed

+243
-83
lines changed

CLAUDE.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Swift-translate is a CLI tool and Swift Package Plugin for localizing iOS/macOS apps by translating String Catalogs (`.xcstrings` files) using OpenAI's GPT models or Google Cloud Translate.
8+
9+
## Common Development Commands
10+
11+
### Build & Test
12+
```bash
13+
swift build # Build the project
14+
swift build -c release # Build for release
15+
swift test # Run all tests
16+
swift test -v # Run tests with verbose output
17+
```
18+
19+
### Running the CLI During Development
20+
```bash
21+
# Basic text translation
22+
swift run swift-translate --verbose -k <API_KEY> --text "Hello" --lang de
23+
24+
# Translate a string catalog
25+
swift run swift-translate -k <API_KEY> path/to/catalog.xcstrings --lang de,fr,it
26+
27+
# Use Google Translate instead of OpenAI
28+
swift run swift-translate -s google -k <API_KEY> path/to/catalog.xcstrings
29+
```
30+
31+
## Architecture & Key Components
32+
33+
### Core Structure
34+
- **SwiftStringCatalog** (`Sources/SwiftStringCatalog/`) - Library for parsing/manipulating `.xcstrings` files
35+
- **SwiftTranslate** (`Sources/SwiftTranslate/`) - Main executable with CLI and translation logic
36+
- **Plugin** (`Plugin/`) - Swift Package Manager plugin support
37+
38+
### Translation Services (`Sources/SwiftTranslate/TranslationServices/`)
39+
- Protocol-based design with `TranslationService` protocol
40+
- `OpenAITranslator` - Supports GPT-4o and GPT-4.1 series models
41+
- `GoogleTranslator` - Google Cloud Translate v2 integration
42+
- Add new services by conforming to `TranslationService` protocol
43+
44+
### String Catalog Models
45+
- Private models (`_StringCatalog`, `_CatalogEntry`) for JSON parsing
46+
- Public models (`LocalizableString`, `LocalizableStringGroup`) for business logic
47+
- Supports plural variations, device variations, and string substitutions
48+
- Respects `shouldTranslate` flag in catalog entries
49+
50+
### CLI Entry Point
51+
- `Sources/SwiftTranslate/Bootstrap/SwiftTranslate.swift` - Main CLI using Swift Argument Parser
52+
- `TranslationCoordinator` orchestrates the translation workflow
53+
- Supports both text and file translation modes
54+
55+
## Key Implementation Notes
56+
57+
### String Catalog Format Support
58+
- Supports Xcode 15, 16, and 26 Beta catalog formats
59+
- Handles complex variations (plurals, devices, substitutions)
60+
- Only translates entries where `shouldTranslate` is true or nil
61+
- Preserves format specifiers like `%@`, `%d`, `%lld`
62+
63+
### Translation Workflow
64+
1. Parses catalog files into internal models
65+
2. Filters strings needing translation
66+
3. Batches requests to translation service
67+
4. Updates catalog with translations
68+
5. Writes to `.loc.xcstrings` files (or overwrites with `--overwrite`)
69+
70+
### Error Handling & Retries
71+
- Custom `SwiftTranslateError` enum for domain errors
72+
- Configurable retry logic for API failures
73+
- Timeout configuration for API requests
74+
- Validation of languages and file paths
75+
76+
### Testing
77+
- Unit tests focus on String Catalog parsing
78+
- Test resources in `Tests/Resources/`
79+
- Use `swift test` to run tests

ExampleCatalogs/BasicCatalog.xcstrings

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{
22
"sourceLanguage" : "en",
33
"strings" : {
4+
"Hidden Spectrum" : {
5+
"comment" : "This should not be translated",
6+
"extractionState" : "manual",
7+
"shouldTranslate" : false
8+
},
49
"I really like tests!" : {
510
"extractionState" : "manual",
611
"localizations" : {

Package.resolved

Lines changed: 24 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ let package = Package(
2424
)
2525
],
2626
dependencies: [
27-
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
28-
.package(url: "https://github.com/MacPaw/OpenAI.git", .upToNextMajor(from: "0.2.9")),
27+
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMajor(from: "1.5.0")),
28+
.package(url: "https://github.com/MacPaw/OpenAI.git", .upToNextMinor(from: "0.4.3")),
2929
.package(url: "https://github.com/onevcat/Rainbow.git", .upToNextMajor(from: "4.0.0")),
3030
],
3131
targets: [

README.md

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
![Swift Translate](https://github.com/hidden-spectrum/swift-translate/assets/469799/1cf0355f-429b-4fa4-9fe1-0b8e777db63e)
22

3-
Swift Translate is a CLI tool and Swift Package Plugin that makes it easy to localize your app. It deconstructs your string catalogs and sends them to OpenAI's GPT-3.5-Turbo model for translation. See it in action:
3+
Swift Translate is a CLI tool and Swift Package Plugin that makes it easy to localize your app. It deconstructs your string catalogs and sends them to OpenAI's GPT-4o / GPT-4.1 series models or Google Cloud Translate (v2) for translation. See it in action:
44

55
https://github.com/hidden-spectrum/swift-translate/assets/469799/ae5066fa-336c-4bab-8f80-1ec5659008d9
66

77
## 📋 Requirements
88
- macOS 13+
99
- Xcode 15+
1010
- Project utilizing [String Catalogs](https://developer.apple.com/videos/play/wwdc2023/10155/) for localization
11-
- [OpenAI API key](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key)
11+
- [OpenAI API key](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) or [Google Cloud Translate (v2)](https://cloud.google.com/translate/docs/overview) API key
1212

1313
## ⭐️ Features
1414
- ✅ Translate individual string catalogs or all catalogs in a folder
15-
- ✅ Translate from English to ar, ca, zh-HK, hr, cs, da, nl, en, fi, fr, de, el, he, hi, hu, id, it, ja, ko, ms, nb, pl, pt-BR, pt-PT, ro, ru, sk, es, sv, th, tr
15+
- ✅ Translate from English to ar, ca, zh-HK, zh-Hans, zh-Hant, hr, cs, da, nl, en, fi, fr, de, el, he, hi, hu, id, it, ja, ko, ms, nb, pl, pt-BR, pt-PT, ro, ru, sk, es, sv, th, tr
1616
- ✅ Support for complex string catalogs with plural & device variations or replacements
1717
- ✅ Translate brand new catalogs or fill in missing translations for existing catalogs
18-
- ✅ Supports ChatGPT (GPT-4o) and Google Translate (v2)
18+
- ✅ Supports ChatGPT (4o and 4.1 series models) and Google Translate (v2)
19+
- ✅ Works with String Catalog formats from Xcode 15, 16, and 26 Beta
1920
- 🚧 Documentation ([#2](/../../issues/2))
2021
- 🚧 Unit tests ([#3](/../../issues/3))
2122
- ❌ Translate from non-English source language ([#23](/../../issues/23))
@@ -24,8 +25,7 @@ https://github.com/hidden-spectrum/swift-translate/assets/469799/ae5066fa-336c-4
2425

2526
## 🛑 Stop Here
2627
Before continuing, please read the following:
27-
- This project is in very early stages. 🐣
28-
- It is **NOT** recommended for production use. ⛔️
28+
- This project is still in development 🚧. While we use it in production for [Dextr](https://dextr.app), exercise caution when using it with your own projects.
2929
- Like any tool built on ChatGPT, responses may be inaccurate or broken completely. 🤪
3030
- Hidden Spectrum is not liable for loss of data, file corruption, or inaccurate/offensive translations (or any subsequent bad app reviews due to aforementioned inaccuracies) 🙅🏻‍♂️
3131

@@ -40,10 +40,10 @@ Ok, with that out of the way let's get into the fun stuff...
4040
**👉 Note:** While this plugin is still in development, this is the recommended way of trying it with your projects.
4141

4242
1. Clone this repository or download a zip from GitHub.
43-
2. Open terminal and `cd` to the repo on your machine.
44-
3. Test your API key with a basic text translation:
43+
2. Open terminal and `cd` to the Swift Translate repo on your machine.
44+
3. Test your OpenAI API key with a basic text translation:
4545
```shell
46-
swift run swift-translate --verbose -k <your key here> --text "This is a test" --lang de
46+
swift run swift-translate --verbose -k <your OpenAI key> --text "This is a test" --lang de
4747
```
4848
4. You should see the following output:
4949

@@ -90,10 +90,6 @@ Ok, with that out of the way let's get into the fun stuff...
9090
🚧 *Not yet supported*
9191

9292

93-
## 💸 A Note on Cost
94-
The current model used in this project, GPT 3.5 Turbo, is extremely cheap. During development of this initial version we executed 3,736 API requests containing 157,734 tokens and our bill came out to just $0.26 USD 😄
95-
96-
9793
## 🙏 Help Wanted
9894
If you're a GPT Guru, we'd love to hear from you about how we can improve our use of the OpenAI API. Open a ticket with your suggestions or [contact us](https://hiddenspectrum.io/contact) to get involved directly.
9995

Sources/SwiftStringCatalog/Bootstrap/StringCatalog.swift

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,8 @@ public final class StringCatalog {
99

1010
// MARK: Public
1111

12-
public enum Error: Swift.Error {
13-
case catalogVersionNotSupported(String)
14-
case substitionsNotYetSupported
15-
}
16-
1712
public let sourceLanguage: Language
18-
public let version = "1.0" // Only version 1.0 supported for now
13+
public let version: String
1914

2015
public private(set) var allKeys = [String]()
2116

@@ -36,10 +31,7 @@ public final class StringCatalog {
3631
let data = try Data(contentsOf: url)
3732
let decoder = JSONDecoder()
3833
let catalog = try decoder.decode(_StringCatalog.self, from: data)
39-
if catalog.version != version {
40-
throw Error.catalogVersionNotSupported(catalog.version)
41-
}
42-
34+
self.version = catalog.version
4335
self.allKeys = Array(catalog.strings.keys)
4436
self.sourceLanguage = catalog.sourceLanguage
4537
self.targetLanguages = {
@@ -53,9 +45,10 @@ public final class StringCatalog {
5345
try loadAllLocalizableStrings(from: catalog)
5446
}
5547

56-
public init(sourceLanguage: Language, targetLanguages: Set<Language> = []) {
48+
public init(sourceLanguage: Language, targetLanguages: Set<Language> = [], version: String = "1.0") {
5749
self.sourceLanguage = sourceLanguage
5850
self.targetLanguages = targetLanguages
51+
self.version = version
5952
}
6053

6154
// MARK: Loading
@@ -83,7 +76,9 @@ public final class StringCatalog {
8376
localizableStringsCount += localizableStrings.count
8477
localizableStringGroups[key] = LocalizableStringGroup(
8578
comment: entry.comment,
86-
extractionState: entry.extractionState,
79+
extractionState: entry.extractionState,
80+
generatesSymbol: entry.generatesSymbol,
81+
shouldTranslate: entry.shouldTranslate,
8782
strings: localizableStrings
8883
)
8984
}
@@ -134,7 +129,8 @@ public final class StringCatalog {
134129
let entries = try buildCatalogEntries()
135130
let catalog = _StringCatalog(
136131
sourceLanguage: sourceLanguage,
137-
strings: entries
132+
strings: entries,
133+
version: version
138134
)
139135
let encoder = JSONEncoder()
140136
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]

Sources/SwiftStringCatalog/Models/Language.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import Foundation
66

77

8-
public struct Language: Codable, Equatable, Hashable, RawRepresentable {
8+
public struct Language: Codable, Equatable, Hashable, RawRepresentable, Sendable {
99

1010
// MARK: Public
1111

@@ -37,6 +37,8 @@ public extension Language {
3737
.arabic,
3838
.catalan,
3939
.chineseHongKong,
40+
.chineseSimplified,
41+
.chineseTraditional,
4042
.croatian,
4143
.czech,
4244
.danish,
@@ -71,6 +73,8 @@ public extension Language {
7173
static let arabic = Self("ar")
7274
static let catalan = Self("ca")
7375
static let chineseHongKong = Self("zh-HK")
76+
static let chineseSimplified = Self("zh-Hans")
77+
static let chineseTraditional = Self("zh-Hant")
7478
static let croatian = Self("hr")
7579
static let czech = Self("cs")
7680
static let danish = Self("da")

Sources/SwiftStringCatalog/Models/LocalizableStringGroup.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,23 @@ public struct LocalizableStringGroup {
1111

1212
public let comment: String?
1313
public let extractionState: ExtractionState?
14+
public let generatesSymbol: Bool?
15+
public let shouldTranslate: Bool?
1416
public let strings: [LocalizableString]
1517

1618
// MARK: Lifecycle
1719

1820
init(
1921
comment: String?,
2022
extractionState: ExtractionState?,
23+
generatesSymbol: Bool?,
24+
shouldTranslate: Bool?,
2125
strings: [LocalizableString]
2226
) {
2327
self.comment = comment
2428
self.extractionState = extractionState
29+
self.generatesSymbol = generatesSymbol
30+
self.shouldTranslate = shouldTranslate
2531
self.strings = strings
2632
}
2733
}

0 commit comments

Comments
 (0)