Skip to content

Commit 9a797f2

Browse files
authored
Merge pull request #40 from kipp-ing/300-de-localization
German localization + Siri phrases (300), on the 1100 Supporter-Unlock tier collapse
2 parents 18b6b07 + 8d9acc0 commit 9a797f2

69 files changed

Lines changed: 4349 additions & 1192 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/scripts/check-english-only.sh‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
#!/bin/bash
22
#
3-
# check-english-only.sh — enforce the project's "English always" rule for UI code.
3+
# check-english-only.sh — enforce the project's "English source" rule for UI code.
44
#
5-
# Scans Swift sources and String Catalogs (.xcstrings) for German and prints any
6-
# offenders. Scoped to code + UI strings on purpose: it does NOT scan Markdown, so
7-
# legacy German specs/docs don't trip it. Exits 2 when German is found so it can be
8-
# wired as a Claude Code hook (exit 2 feeds the message back to the agent); exits 0
9-
# when clean. Safe to run manually or in CI too.
5+
# The source language is English: all Swift string literals, comments, and code stay
6+
# English. Translations do NOT live in Swift — they live in the String Catalogs
7+
# (.xcstrings), which is the one sanctioned place for other languages. So this script
8+
# scans *Swift sources* for German and leaves the catalogs alone.
9+
#
10+
# Policy change (topic-300, 2026-07-23): German UI shipping is now enabled via the
11+
# String Catalogs, so the former check that blocked any "de" localization in
12+
# .xcstrings was removed. The remaining checks still guarantee that German never
13+
# leaks into Swift source — it must be added as a catalog translation, never inline.
14+
#
15+
# Exits 2 when German is found in Swift so it can be wired as a Claude Code hook
16+
# (exit 2 feeds the message back to the agent); exits 0 when clean. Safe in CI too.
1017
#
1118
# Deliberate exception: the diacritic test fixture "München Trip" (AlbumSearch
1219
# folded-search coverage) is allow-listed.
@@ -20,13 +27,7 @@ cd "$ROOT" || exit 0
2027
fail=0
2128
report() { echo "$1" >&2; echo "$2" >&2; fail=1; }
2229

23-
# 1) German "de" localizations in String Catalogs — definitive (source language is en).
24-
de_hits="$(grep -rn '"de" :' --include='*.xcstrings' . 2>/dev/null | grep -v '/.build/' || true)"
25-
[ -n "$de_hits" ] && report \
26-
"✗ German 'de' localizations in String Catalogs (remove them — English is the source language):" \
27-
"$de_hits"
28-
29-
# 2) German umlauts in Swift (high-signal). Allow the intentional "München" test fixture.
30+
# German umlauts in Swift (high-signal). Allow the intentional "München" test fixture.
3031
umlaut_hits="$(grep -rnE '[äöüÄÖÜß]' --include='*.swift' . 2>/dev/null | grep -v '/.build/' | grep -v 'München' || true)"
3132
[ -n "$umlaut_hits" ] && report \
3233
"✗ German umlauts in Swift (translate to English):" \

‎OwnFrame.xcodeproj/project.pbxproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@
412412
knownRegions = (
413413
en,
414414
Base,
415+
de,
415416
);
416417
mainGroup = 3DDA11552FE2D16C0059B8FD;
417418
minimizedProjectReferenceProxies = 1;

‎OwnFrame/AppShortcuts.xcstrings‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{
2+
"sourceLanguage" : "en",
3+
"strings" : {
4+
"Get ${applicationName} state" : {
5+
"comment" : "Spoken Siri phrase for the Get Frame State shortcut. Every utterance must contain ${applicationName}.",
6+
"extractionState" : "manual",
7+
"localizations" : {
8+
"de" : {
9+
"stringUnit" : {
10+
"state" : "translated",
11+
"value" : "Status von ${applicationName} abrufen"
12+
}
13+
}
14+
}
15+
},
16+
"Next photo on ${applicationName}" : {
17+
"comment" : "Spoken Siri phrase for the Next Photo shortcut. Every utterance must contain ${applicationName}.",
18+
"extractionState" : "manual",
19+
"localizations" : {
20+
"de" : {
21+
"stringUnit" : {
22+
"state" : "translated",
23+
"value" : "Nächstes Foto auf ${applicationName}"
24+
}
25+
}
26+
}
27+
},
28+
"Pause ${applicationName}" : {
29+
"comment" : "Spoken Siri phrase for the Pause Slideshow shortcut. Every utterance must contain ${applicationName}.",
30+
"extractionState" : "manual",
31+
"localizations" : {
32+
"de" : {
33+
"stringUnit" : {
34+
"state" : "translated",
35+
"value" : "Pausiere ${applicationName}"
36+
}
37+
}
38+
}
39+
},
40+
"Previous photo on ${applicationName}" : {
41+
"comment" : "Spoken Siri phrase for the Previous Photo shortcut. Every utterance must contain ${applicationName}.",
42+
"extractionState" : "manual",
43+
"localizations" : {
44+
"de" : {
45+
"stringUnit" : {
46+
"state" : "translated",
47+
"value" : "Vorheriges Foto auf ${applicationName}"
48+
}
49+
}
50+
}
51+
},
52+
"Resume ${applicationName}" : {
53+
"comment" : "Spoken Siri phrase for the Resume Slideshow shortcut. Every utterance must contain ${applicationName}.",
54+
"extractionState" : "manual",
55+
"localizations" : {
56+
"de" : {
57+
"stringUnit" : {
58+
"state" : "translated",
59+
"value" : "Setze ${applicationName} fort"
60+
}
61+
}
62+
}
63+
},
64+
"Set ${applicationName} brightness" : {
65+
"comment" : "Spoken Siri phrase for the Set Frame Brightness shortcut. Every utterance must contain ${applicationName}.",
66+
"extractionState" : "manual",
67+
"localizations" : {
68+
"de" : {
69+
"stringUnit" : {
70+
"state" : "translated",
71+
"value" : "Helligkeit von ${applicationName} einstellen"
72+
}
73+
}
74+
}
75+
},
76+
"Set ${applicationName} source" : {
77+
"comment" : "Spoken Siri phrase for the Set Frame Source shortcut. Every utterance must contain ${applicationName}.",
78+
"extractionState" : "manual",
79+
"localizations" : {
80+
"de" : {
81+
"stringUnit" : {
82+
"state" : "translated",
83+
"value" : "Quelle von ${applicationName} festlegen"
84+
}
85+
}
86+
}
87+
}
88+
},
89+
"version" : "1.0"
90+
}

‎OwnFrame/Intents/FrameIntentContext.swift‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ enum FrameIntentContext {
3030
return registry
3131
}
3232

33-
/// The `.automation` guard every intent runs FIRST (1100, data-model.md §Gated feature
33+
/// The Supporter-Unlock guard every intent runs FIRST (1100, data-model.md §Gated feature
3434
/// mapping). Ahead of `requireRegistry()` on purpose: an unentitled *and* unconfigured
3535
/// frame must report the unlock, not send the owner off to fix a setup that was never
3636
/// the problem. Running it first also keeps a locked intent inert — it never moves the
3737
/// frame and then complains.
38-
static func requireAutomation() throws(FrameIntentError) {
39-
guard entitlements().contains(.automation) else { throw .automationLocked }
38+
static func requireSupporterUnlock() throws(FrameIntentError) {
39+
guard entitlements().contains(.supporter) else { throw .supporterRequired }
4040
}
4141
}

‎OwnFrame/Intents/FrameIntents.swift‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// validation, and parity live in AppIntentsKit where they are host-tested;
99
// nothing here may grow beyond gate → resolve → forward → map the error.
1010
//
11-
// 1100: every perform() opens with `requireAutomation()`. The intents stay LISTED in
11+
// 1100: every perform() opens with `requireSupporterUnlock()`. The intents stay LISTED in
1212
// Shortcuts while locked — hiding them would strand shortcuts an owner already built and
1313
// make the capability undiscoverable; a locked intent is a discoverable intent that fails
1414
// with a readable unlock message.
@@ -25,9 +25,9 @@ enum FrameIntentError: Error, Equatable, CustomLocalizedStringResourceConvertibl
2525
case frameNotOpen
2626
case brightnessOutOfRange
2727
case sourceMissing
28-
/// 1100: the frame does not own the Automation unlock. Not a mapping of any
28+
/// 1100: the frame does not own the Supporter Unlock. Not a mapping of any
2929
/// `FrameCommandError` — it is raised before the command layer is reached at all.
30-
case automationLocked
30+
case supporterRequired
3131

3232
init(_ error: FrameCommandError) {
3333
switch error {
@@ -48,10 +48,10 @@ enum FrameIntentError: Error, Equatable, CustomLocalizedStringResourceConvertibl
4848
return "Brightness must be between 0 and 100 percent."
4949
case .sourceMissing:
5050
return "This source no longer exists in the frame's library."
51-
case .automationLocked:
51+
case .supporterRequired:
5252
return LocalizedStringResource(
53-
"unlock.required.automation",
54-
defaultValue: "Remote control requires the Automation unlock."
53+
"unlock.required.supporter",
54+
defaultValue: "Remote control requires the Supporter Unlock."
5555
)
5656
}
5757
}
@@ -64,7 +64,7 @@ struct PauseSlideshowIntent: AppIntent {
6464

6565
@MainActor
6666
func perform() async throws -> some IntentResult {
67-
try FrameIntentContext.requireAutomation()
67+
try FrameIntentContext.requireSupporterUnlock()
6868
let registry = try FrameIntentContext.requireRegistry()
6969
do { try await FrameCommandService(registry: registry).pause() }
7070
catch { throw FrameIntentError(error) }
@@ -79,7 +79,7 @@ struct ResumeSlideshowIntent: AppIntent {
7979

8080
@MainActor
8181
func perform() async throws -> some IntentResult {
82-
try FrameIntentContext.requireAutomation()
82+
try FrameIntentContext.requireSupporterUnlock()
8383
let registry = try FrameIntentContext.requireRegistry()
8484
do { try await FrameCommandService(registry: registry).resume() }
8585
catch { throw FrameIntentError(error) }
@@ -94,7 +94,7 @@ struct NextPhotoIntent: AppIntent {
9494

9595
@MainActor
9696
func perform() async throws -> some IntentResult {
97-
try FrameIntentContext.requireAutomation()
97+
try FrameIntentContext.requireSupporterUnlock()
9898
let registry = try FrameIntentContext.requireRegistry()
9999
do { try await FrameCommandService(registry: registry).nextPhoto() }
100100
catch { throw FrameIntentError(error) }
@@ -109,7 +109,7 @@ struct PreviousPhotoIntent: AppIntent {
109109

110110
@MainActor
111111
func perform() async throws -> some IntentResult {
112-
try FrameIntentContext.requireAutomation()
112+
try FrameIntentContext.requireSupporterUnlock()
113113
let registry = try FrameIntentContext.requireRegistry()
114114
do { try await FrameCommandService(registry: registry).previousPhoto() }
115115
catch { throw FrameIntentError(error) }
@@ -131,7 +131,7 @@ struct SelectSourceIntent: AppIntent {
131131

132132
@MainActor
133133
func perform() async throws -> some IntentResult {
134-
try FrameIntentContext.requireAutomation()
134+
try FrameIntentContext.requireSupporterUnlock()
135135
let registry = try FrameIntentContext.requireRegistry()
136136
do { try await FrameCommandService(registry: registry).selectSource(id: source.id, label: source.label) }
137137
catch { throw FrameIntentError(error) }
@@ -148,7 +148,7 @@ struct GetFrameStateIntent: AppIntent {
148148

149149
@MainActor
150150
func perform() async throws -> some IntentResult & ReturnsValue<FrameStateEntity> {
151-
try FrameIntentContext.requireAutomation()
151+
try FrameIntentContext.requireSupporterUnlock()
152152
let registry = try FrameIntentContext.requireRegistry()
153153
do {
154154
let snapshot = try await FrameCommandService(registry: registry).frameState()
@@ -173,7 +173,7 @@ struct SetBrightnessIntent: AppIntent {
173173

174174
@MainActor
175175
func perform() async throws -> some IntentResult {
176-
try FrameIntentContext.requireAutomation()
176+
try FrameIntentContext.requireSupporterUnlock()
177177
let registry = try FrameIntentContext.requireRegistry()
178178
do { try await FrameCommandService(registry: registry).setBrightness(percent: brightness) }
179179
catch { throw FrameIntentError(error) }

‎OwnFrame/Intents/FrameStateEntity.swift‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ struct FrameStateEntity: TransientAppEntity {
3333
var photoCountry: String?
3434

3535
var displayRepresentation: DisplayRepresentation {
36-
DisplayRepresentation(
37-
title: "\(isPlaying ? "Playing" : "Paused") · \(brightnessPercent)%",
36+
// The status word is localized on its own so Siri and the Shortcuts result
37+
// read it in the device language; the "%@ · %lld%%" frame is localized too.
38+
let status: LocalizedStringResource = isPlaying ? "Playing" : "Paused"
39+
return DisplayRepresentation(
40+
title: "\(String(localized: status)) · \(brightnessPercent)%",
3841
subtitle: sourceLabel.map { "\($0)" }
3942
)
4043
}

0 commit comments

Comments
 (0)