Skip to content

Commit c59cf00

Browse files
committed
feat(ios): generate SupportedLocales at build time via SwiftPM plugin
Introduce a `BuildToolPlugin` that reads the JS-emitted `supported-locales.json` manifest before `GutenbergKit` compiles and emits an `internal enum SupportedLocales { static let all: Set<String> }` constant. The resolver consumes the constant directly, dropping the runtime IO that the previous commit shipped. This mirrors the Android sibling's `:Gutenberg:generateSupportedLocales` gradle task: a missing manifest fails the build (SwiftPM reports it as a missing input), so a release that silently degrades every consumer to English is unreachable in shipped artifacts. SwiftPM's incremental graph treats the manifest as an input file, so changes from `make build` trigger regeneration without rebuilding the whole target. Layout: - `ios/Plugins/SupportedLocalesPlugin/` — the plugin itself; computes the manifest URL via `context.package.directoryURL` and wires up a buildCommand against the generator tool. - `ios/Plugins/GenerateSupportedLocales/` — small `executableTarget` the plugin invokes. Reads the manifest, validates it's a JSON array of strings, writes the Swift source. Friendly error messages for missing/malformed input as a backstop in case SwiftPM's prebuild check is bypassed. `GutenbergKitResources.loadSupportedLocales()` is removed since it now has no callers.
1 parent 6f04e11 commit c59cf00

6 files changed

Lines changed: 140 additions & 38 deletions

File tree

Package.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,18 @@ let package = Package(
3030
dependencies: ["SwiftSoup", "SVGView", "GutenbergKitResources"],
3131
path: "ios/Sources/GutenbergKit",
3232
exclude: ["Gutenberg"],
33-
packageAccess: false
33+
packageAccess: false,
34+
plugins: ["SupportedLocalesPlugin"]
35+
),
36+
.executableTarget(
37+
name: "GenerateSupportedLocales",
38+
path: "ios/Plugins/GenerateSupportedLocales"
39+
),
40+
.plugin(
41+
name: "SupportedLocalesPlugin",
42+
capability: .buildTool(),
43+
dependencies: ["GenerateSupportedLocales"],
44+
path: "ios/Plugins/SupportedLocalesPlugin"
3445
),
3546
.target(
3647
name: "GutenbergKitHTTP",
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import Foundation
2+
3+
/// Reads `supported-locales.json` (a JSON array of locale tags emitted by the
4+
/// Vite build) and writes a Swift source file declaring an
5+
/// `internal enum SupportedLocales { static let all: Set<String> }` constant.
6+
///
7+
/// Invoked by `SupportedLocalesPlugin` as a buildCommand; not intended to be
8+
/// run directly.
9+
@main
10+
struct GenerateSupportedLocales {
11+
static func main() {
12+
let args = CommandLine.arguments
13+
guard args.count == 3 else {
14+
fail("usage: GenerateSupportedLocales <manifest.json> <output.swift>")
15+
}
16+
17+
let manifestURL = URL(fileURLWithPath: args[1])
18+
let outputURL = URL(fileURLWithPath: args[2])
19+
20+
guard FileManager.default.fileExists(atPath: manifestURL.path) else {
21+
fail(
22+
"missing supported-locales manifest at \(manifestURL.path). "
23+
+ "Run `make build` to regenerate it."
24+
)
25+
}
26+
27+
let data: Data
28+
do {
29+
data = try Data(contentsOf: manifestURL)
30+
} catch {
31+
fail("could not read \(manifestURL.path): \(error.localizedDescription)")
32+
}
33+
34+
let locales: [String]
35+
do {
36+
locales = try JSONDecoder().decode([String].self, from: data)
37+
} catch {
38+
fail(
39+
"manifest at \(manifestURL.path) is not a JSON array of strings: "
40+
+ "\(error.localizedDescription)"
41+
)
42+
}
43+
44+
let entries = Set(locales).sorted()
45+
.map { " \"\($0)\"" }
46+
.joined(separator: ",\n")
47+
48+
let source = """
49+
// Generated by SupportedLocalesPlugin from supported-locales.json.
50+
// Do not edit.
51+
52+
internal enum SupportedLocales {
53+
static let all: Set<String> = [
54+
\(entries)
55+
]
56+
}
57+
58+
"""
59+
60+
do {
61+
try source.write(to: outputURL, atomically: true, encoding: .utf8)
62+
} catch {
63+
fail("could not write \(outputURL.path): \(error.localizedDescription)")
64+
}
65+
}
66+
67+
private static func fail(_ message: String) -> Never {
68+
FileHandle.standardError.write(Data("error: \(message)\n".utf8))
69+
exit(1)
70+
}
71+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Foundation
2+
import PackagePlugin
3+
4+
/// Generates `SupportedLocales.swift` from the JS-emitted manifest before
5+
/// `GutenbergKit` is compiled.
6+
///
7+
/// The Vite build writes `dist/supported-locales.json` and `make
8+
/// copy-dist-ios` copies it to
9+
/// `ios/Sources/GutenbergKitResources/Gutenberg/supported-locales.json`.
10+
/// This plugin reads that file at build time and emits an
11+
/// `internal enum SupportedLocales { static let all: Set<String> }`
12+
/// constant the resolver can use without runtime IO.
13+
///
14+
/// A missing manifest fails the build with a message pointing at `make
15+
/// build`, so the silent-fall-through-to-English failure mode is unreachable
16+
/// in shipped artifacts.
17+
@main
18+
struct SupportedLocalesPlugin: BuildToolPlugin {
19+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
20+
let manifestURL = context.package.directoryURL
21+
.appending(path: "ios/Sources/GutenbergKitResources/Gutenberg/supported-locales.json")
22+
let outputURL = context.pluginWorkDirectoryURL
23+
.appending(path: "SupportedLocales.swift")
24+
let tool = try context.tool(named: "GenerateSupportedLocales")
25+
26+
return [
27+
.buildCommand(
28+
displayName: "Generate SupportedLocales.swift",
29+
executable: tool.url,
30+
arguments: [
31+
manifestURL.path(percentEncoded: false),
32+
outputURL.path(percentEncoded: false),
33+
],
34+
inputFiles: [manifestURL],
35+
outputFiles: [outputURL]
36+
)
37+
]
38+
}
39+
}

ios/Sources/GutenbergKit/Sources/Model/LocaleResolver.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import Foundation
2-
import GutenbergKitResources
32

43
/// Resolves an arbitrary locale tag to one of the bundles GutenbergKit
54
/// actually ships translations for.
@@ -27,16 +26,23 @@ import GutenbergKitResources
2726
/// (e.g. `de-DE-u-ca-gregory`) are ignored — the editor doesn't vary
2827
/// translations by calendar or numbering system.
2928
///
30-
/// The supported set is read from a manifest emitted by the JS build, so
31-
/// it stays in sync with what the bundle actually ships.
29+
/// The supported set is generated at build time from the JS build manifest
30+
/// (see `SupportedLocalesPlugin`), so the resolver and the shipped bundles
31+
/// cannot drift.
3232
struct LocaleResolver {
3333
static let `default` = LocaleResolver()
3434

3535
private let supportedLocales: Set<String>
3636

3737
init(supportedLocales: [String]? = nil) {
38-
let source = supportedLocales ?? GutenbergKitResources.loadSupportedLocales()
39-
self.supportedLocales = Set(source.map { Self.normalize($0) })
38+
if let supportedLocales {
39+
self.supportedLocales = Set(supportedLocales.map(Self.normalize))
40+
} else {
41+
// The build-time-generated set is already lowercase-with-dashes
42+
// (JS emits the manifest in that form), so no normalisation is
43+
// needed for the default path.
44+
self.supportedLocales = SupportedLocales.all
45+
}
4046
}
4147

4248
/// Resolves a string locale tag against the shipped translation bundles.

ios/Sources/GutenbergKitResources/Sources/GutenbergKitResources.swift

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,6 @@ public enum GutenbergKitResources {
2727
return url
2828
}
2929

30-
/// Loads the list of locale tags for which the bundle ships translations.
31-
///
32-
/// The list is generated at JS build time by scanning `src/translations/`,
33-
/// so it is the single source of truth for "what do we actually ship?".
34-
/// Returns an empty array when the manifest is missing — callers should
35-
/// treat that as "no shipped translations" and fall back to the default
36-
/// locale rather than crashing.
37-
///
38-
/// - Returns: The shipped locale tags (e.g. `["ar", "de", "pt-br", "zh-cn"]`).
39-
public static func loadSupportedLocales() -> [String] {
40-
guard let url = Bundle.module.url(
41-
forResource: "supported-locales",
42-
withExtension: "json",
43-
subdirectory: "Gutenberg"
44-
) else {
45-
return []
46-
}
47-
guard let data = try? Data(contentsOf: url),
48-
let locales = try? JSONDecoder().decode([String].self, from: data) else {
49-
return []
50-
}
51-
return locales
52-
}
53-
5430
/// Loads the Gutenberg CSS from the bundled assets.
5531
///
5632
/// Scans the `Gutenberg/assets/` directory for the Vite-generated

ios/Tests/GutenbergKitTests/Model/LocaleResolverTests.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import Foundation
2-
import GutenbergKitResources
32
import Testing
43

54
@testable import GutenbergKit
@@ -118,15 +117,15 @@ struct LocaleResolverTests {
118117

119118
// MARK: - Exhaustive coverage of the shipped manifest
120119

121-
// The default resolver reads the manifest emitted by `make build`. Tests
122-
// run after `make build` (see Makefile), so the list reflects what the
123-
// bundle actually ships. Each parameterised case asserts the round-trip
124-
// contract: a shipped tag must resolve to itself — no normalisation
125-
// tricks, no accidental fallbacks.
120+
// `SupportedLocales.all` is generated at build time by
121+
// `SupportedLocalesPlugin` from the JS-emitted manifest, so the set
122+
// reflects what the bundle actually ships. Each parameterised case
123+
// asserts the round-trip contract: a shipped tag must resolve to
124+
// itself — no normalisation tricks, no accidental fallbacks.
126125

127-
static let shippedLocales = GutenbergKitResources.loadSupportedLocales()
126+
static let shippedLocales = Array(SupportedLocales.all).sorted()
128127

129-
@Test("Manifest is non-empty (run `make build` first)")
128+
@Test("SupportedLocales.all is non-empty (plugin should fail the build otherwise)")
130129
func manifestPresent() {
131130
#expect(!Self.shippedLocales.isEmpty)
132131
}

0 commit comments

Comments
 (0)