package diff import ( "fmt" "os" "path/filepath" "strings" "testing" "github.com/blacktop/go-plist" ) func TestDiffLocalizedResourcesReportsUpdatedLoctableKeys(t *testing.T) { path := "FileSystem/System/Library/PrivateFrameworks/CommunicationsFilter.framework/CommunicationsFilter.loctable" oldResources := map[string]string{ path: `en.%d_ADDITIONAL_CONTACTS_BLOCKED_ALERT_TITLE.value.one = "%d other contact was unblocked."`, } newResources := map[string]string{ path: strings.Join([]string{ `en.%d_ADDITIONAL_CONTACTS_BLOCKED_ALERT_TITLE.value.one = "%d other contact was unblocked."`, `en.BLOCKLIST_LIMIT_ALERT_TITLE = "Blocked Contacts Limit Reached"`, }, "\n"), } out := &PlistDiff{ New: make(map[string]string), Updated: make(map[string]string), } if err := diffLocalizedResources(out, oldResources, newResources); err != nil { t.Fatalf("diffLocalizedResources returned error: %v", err) } got := out.Updated[path] if !strings.Contains(got, `+en.BLOCKLIST_LIMIT_ALERT_TITLE = "Blocked Contacts Limit Reached"`) { t.Fatalf("updated localization diff missing blocklist alert title:\n%s", got) } } func TestLocalizationMarkdownFilenameUsesFullPath(t *testing.T) { first := plistMarkdownFilename("FileSystem/Applications/Phone.app/en.lproj/Localizable.strings", localizationDisplayName) second := plistMarkdownFilename("FileSystem/System/Library/Frameworks/Test.framework/en.lproj/Localizable.strings", localizationDisplayName) if first == second { t.Fatalf("localizationMarkdownFilename collided for same basename: %q", first) } if !strings.HasSuffix(first, "_Phone.md") { t.Fatalf("plistMarkdownFilename(%q) should use containing bundle name", first) } if !strings.HasSuffix(second, "_Test.md") { t.Fatalf("plistMarkdownFilename(%q) should use containing bundle name", second) } } func TestLocalizationDisplayNameUsesContainingBundle(t *testing.T) { tests := []struct { path string want string }{ { path: "FileSystem/Applications/CarPlaySettings.app/Localizable.loctable", want: "CarPlaySettings", }, { path: "FileSystem/System/Library/PrivateFrameworks/ActionKit.framework/Localizable.loctable", want: "ActionKit", }, { path: "FileSystem/System/Library/PrivateFrameworks/NearFieldPrivateServices.framework/XPCServices/NFLocationService.xpc/InfoPlist.loctable", want: "NFLocationService", }, { path: "FileSystem/System/Library/OnBoardingBundles/com.apple.onboarding.analyticsdevice.bundle/AnalyticsDevice.loctable", want: "com.apple.onboarding.analyticsdevice", }, { path: "FileSystem/usr/share/Localizable.loctable", want: "Localizable", }, } for _, tt := range tests { if got := localizationDisplayName(tt.path); got != tt.want { t.Fatalf("localizationDisplayName(%q) = %q, want %q", tt.path, got, tt.want) } } } func TestMarkdownUsesLocalizationDisplayName(t *testing.T) { path := "FileSystem/System/Library/PrivateFrameworks/ActionKit.framework/Localizable.loctable" output := t.TempDir() d := New(&Config{ Title: "Localization Test", IpswOld: "old.ipsw", IpswNew: "new.ipsw", Output: output, }) d.Localizations = map[string]*PlistDiff{ "SystemOS": { New: map[string]string{ path: `en.KEY = "value"`, }, }, } if err := d.Markdown(); err != nil { t.Fatalf("Markdown returned error: %v", err) } readme, err := os.ReadFile(filepath.Join(output, d.TitleToFilename(), "README.md")) if err != nil { t.Fatalf("failed to read Markdown output: %v", err) } rendered := string(readme) if !strings.Contains(rendered, "##### ActionKit") { t.Fatalf("rendered Markdown should use localization owner name:\n%s", rendered) } if strings.Contains(rendered, "##### Localizable.loctable") { t.Fatalf("rendered Markdown should not use generic localization resource basename:\n%s", rendered) } } func TestMarkdownNewLocalizationOverflowFilesAreFenced(t *testing.T) { output := t.TempDir() d := New(&Config{ Title: "Localization Overflow Test", IpswOld: "old.ipsw", IpswNew: "new.ipsw", Output: output, }) // >= 20 new resources forces the overflow path that writes per-resource // Markdown files instead of inlining the content. volumeDiff := &PlistDiff{New: make(map[string]string)} for i := range 25 { path := fmt.Sprintf("FileSystem/System/Library/PrivateFrameworks/Frame%02d.framework/Localizable.loctable", i) volumeDiff.New[path] = fmt.Sprintf("en.KEY_A = \"value %d\"\nen.KEY_B = \"other %d\"", i, i) } d.Localizations = map[string]*PlistDiff{"SystemOS": volumeDiff} if err := d.Markdown(); err != nil { t.Fatalf("Markdown returned error: %v", err) } locDir := filepath.Join(output, d.TitleToFilename(), "LOCALIZATIONS") entries, err := os.ReadDir(locDir) if err != nil { t.Fatalf("failed to read LOCALIZATIONS dir: %v", err) } if len(entries) == 0 { t.Fatalf("expected overflow localization files in %s", locDir) } body, err := os.ReadFile(filepath.Join(locDir, entries[0].Name())) if err != nil { t.Fatalf("failed to read overflow file: %v", err) } if !strings.Contains(string(body), "```text\n") { t.Fatalf("overflow localization file should fence flattened content as a code block:\n%s", body) } } func TestNormalizeLocalizedResourceFlattensLoctable(t *testing.T) { resource := map[string]any{ "ar": map[string]any{ "BLOCKLIST_LIMIT_ALERT_TITLE": "Arabic title", }, "en": map[string]any{ "BLOCKLIST_LIMIT_ALERT_TITLE": "Blocked Contacts Limit Reached", "%d_ADDITIONAL_CONTACTS_BLOCKED_ALERT_TITLE": map[string]any{ "NSStringLocalizedFormatKey": "%#@value@", "value": map[string]any{ "one": "%d other contact who shares the same phone number or email address was unblocked.", "other": "%d other contacts who share the same phone number or email address were unblocked.", }, }, }, "en_GB": map[string]any{ "BLOCKLIST_LIMIT_ALERT_TITLE": "Blocked Contacts Limit Reached", }, } data, err := plist.Marshal(resource, plist.BinaryFormat) if err != nil { t.Fatalf("failed to marshal test plist: %v", err) } path := t.TempDir() + "/CommunicationsFilter.loctable" if err := os.WriteFile(path, data, 0o644); err != nil { t.Fatalf("failed to write test loctable: %v", err) } normalized, err := normalizeLocalizedResource(path) if err != nil { t.Fatalf("normalizeLocalizedResource returned error: %v", err) } for _, needle := range []string{ `en.BLOCKLIST_LIMIT_ALERT_TITLE = "Blocked Contacts Limit Reached"`, `en.%d_ADDITIONAL_CONTACTS_BLOCKED_ALERT_TITLE.value.one = "%d other contact who shares the same phone number or email address was unblocked."`, } { if !strings.Contains(normalized, needle) { t.Fatalf("normalized resource missing %q:\n%s", needle, normalized) } } for _, needle := range []string{`ar.`, `en_GB.`} { if strings.Contains(normalized, needle) { t.Fatalf("normalized resource should only include US English, found %q:\n%s", needle, normalized) } } } func TestNormalizeLocalizedResourceFallsBackToAvailableEnglishLoctable(t *testing.T) { resource := map[string]any{ "en_AU": map[string]any{ "CFBundleName": "NFLocationService", }, "en_GB": map[string]any{ "CFBundleName": "NFLocationService", }, "fr": map[string]any{ "CFBundleName": "Service NFC", }, "LocProvenance": map[string]any{ "en_AU": uint64(1), }, } data, err := plist.Marshal(resource, plist.BinaryFormat) if err != nil { t.Fatalf("failed to marshal test plist: %v", err) } path := t.TempDir() + "/InfoPlist.loctable" if err := os.WriteFile(path, data, 0o644); err != nil { t.Fatalf("failed to write test loctable: %v", err) } normalized, err := normalizeLocalizedResource(path) if err != nil { t.Fatalf("normalizeLocalizedResource returned error: %v", err) } if !strings.Contains(normalized, `en.CFBundleName = "NFLocationService"`) { t.Fatalf("normalized resource missing fallback English locale:\n%s", normalized) } for _, needle := range []string{"en_AU.", "en_GB.", "fr.", "LocProvenance"} { if strings.Contains(normalized, needle) { t.Fatalf("normalized resource should include one English fallback, found %q:\n%s", needle, normalized) } } } func TestNormalizeLocalizedResourcePrefersUSEnglishLoctable(t *testing.T) { resource := map[string]any{ "en": map[string]any{ "BLOCKLIST_LIMIT_ALERT_TITLE": "Generic English title", }, "en_US": map[string]any{ "BLOCKLIST_LIMIT_ALERT_TITLE": "Blocked Contacts Limit Reached", }, "en_GB": map[string]any{ "BLOCKLIST_LIMIT_ALERT_TITLE": "Blocked Contacts Limit Reached", }, } data, err := plist.Marshal(resource, plist.BinaryFormat) if err != nil { t.Fatalf("failed to marshal test plist: %v", err) } path := t.TempDir() + "/CommunicationsFilter.loctable" if err := os.WriteFile(path, data, 0o644); err != nil { t.Fatalf("failed to write test loctable: %v", err) } normalized, err := normalizeLocalizedResource(path) if err != nil { t.Fatalf("normalizeLocalizedResource returned error: %v", err) } if !strings.Contains(normalized, `en.BLOCKLIST_LIMIT_ALERT_TITLE = "Blocked Contacts Limit Reached"`) { t.Fatalf("normalized resource should prefer US English:\n%s", normalized) } for _, needle := range []string{`Generic English title`, `en_US.`, `en_GB.`} { if strings.Contains(normalized, needle) { t.Fatalf("normalized resource should include one US English lane, found %q:\n%s", needle, normalized) } } } func TestUSEnglishLocalizedResourcePathFilter(t *testing.T) { tests := []struct { path string want bool }{ {path: "System/Library/Foo.framework/en.lproj/Localizable.strings", want: true}, {path: "System/Library/Foo.framework/en_US.lproj/Localizable.strings", want: true}, {path: "System/Library/Foo.framework/en-US.lproj/Localizable.strings", want: true}, {path: "System/Library/Foo.framework/Base.lproj/Localizable.strings", want: true}, {path: "System/Library/Foo.framework/en_GB.lproj/Localizable.strings", want: false}, {path: "System/Library/Foo.framework/fr.lproj/Localizable.strings", want: false}, {path: "System/Library/Foo.framework/CommunicationsFilter.loctable", want: true}, } for _, tt := range tests { if got := isUSEnglishLocalizedResourcePath(tt.path); got != tt.want { t.Fatalf("isUSEnglishLocalizedResourcePath(%q) = %t, want %t", tt.path, got, tt.want) } } }