11package main
22
33import (
4+ "flag"
45 "fmt"
6+ "os"
7+ "path/filepath"
8+ "regexp"
9+ "sort"
10+ "strings"
11+
12+ "gopkg.in/yaml.v3"
513)
614
715func runManifest (args []string ) error {
16+ fs := flag .NewFlagSet ("manifest" , flag .ExitOnError )
17+ crossValidate := fs .Bool ("cross-validate" , false , "Also check locale registrations in command-api.yaml" )
18+ fs .Parse (args )
19+
820 root , err := repoRoot ()
921 if err != nil {
1022 return err
@@ -21,5 +33,174 @@ func runManifest(args []string) error {
2133 fmt .Printf (" %-12s %s\n " , loc .Code , loc .Status )
2234 }
2335 fmt .Println ("Manifest valid." )
36+
37+ if * crossValidate {
38+ return crossValidateManifest (root , m )
39+ }
2440 return nil
2541}
42+
43+ // crossValidateManifest checks that the manifest's locale list matches
44+ // the locale enum in command-api.yaml.
45+ func crossValidateManifest (root string , m * Manifest ) error {
46+ apiPath := translationsPath (root , "../specs/command-api.yaml" )
47+ apiLocales , err := parseAPILocaleEnum (apiPath )
48+ if err != nil {
49+ return err
50+ }
51+
52+ // Build expected set: "none" plus all manifest locales.
53+ expected := make (map [string ]bool )
54+ expected ["none" ] = true
55+ for code := range m .Locales {
56+ expected [code ] = true
57+ }
58+
59+ apiSet := make (map [string ]bool )
60+ for _ , code := range apiLocales {
61+ apiSet [code ] = true
62+ }
63+
64+ var errors []string
65+
66+ // Locales in manifest but missing from API.
67+ for code := range expected {
68+ if ! apiSet [code ] {
69+ errors = append (errors , fmt .Sprintf (" manifest locale %q missing from command-api.yaml enum" , code ))
70+ }
71+ }
72+
73+ // Locales in API but not in manifest (excluding "none").
74+ for code := range apiSet {
75+ if ! expected [code ] {
76+ errors = append (errors , fmt .Sprintf (" command-api.yaml enum has %q not in manifest" , code ))
77+ }
78+ }
79+
80+ // Validate settingsValidator.ts uses dynamic locale discovery.
81+ validatorPath := filepath .Join (root , "pkg" , "rancher-desktop" , "main" ,
82+ "commandServer" , "settingsValidator.ts" )
83+ if validatorData , err := os .ReadFile (validatorPath ); err != nil {
84+ errors = append (errors , fmt .Sprintf (" cannot read settingsValidator.ts: %v" , err ))
85+ } else {
86+ content := string (validatorData )
87+ if ! strings .Contains (content , "...availableLocales" ) {
88+ errors = append (errors , " settingsValidator.ts: locale checkEnum does not use ...availableLocales (hardcoded list?)" )
89+ }
90+ }
91+
92+ // Validate settingsValidator.spec.ts test values against manifest.
93+ specPath := filepath .Join (root , "pkg" , "rancher-desktop" , "main" ,
94+ "commandServer" , "__tests__" , "settingsValidator.spec.ts" )
95+ if specData , err := os .ReadFile (specPath ); err != nil {
96+ errors = append (errors , fmt .Sprintf (" cannot read settingsValidator.spec.ts: %v" , err ))
97+ } else {
98+ specErrors := crossValidateSpec (string (specData ), expected )
99+ errors = append (errors , specErrors ... )
100+ }
101+
102+ sort .Strings (errors )
103+
104+ if len (errors ) > 0 {
105+ fmt .Println ("\n Cross-validation errors:" )
106+ for _ , e := range errors {
107+ fmt .Println (e )
108+ }
109+ return fmt .Errorf ("cross-validation failed" )
110+ }
111+
112+ fmt .Println ("Cross-validation passed." )
113+ return nil
114+ }
115+
116+ // parseAPILocaleEnum extracts the locale enum values from command-api.yaml
117+ // by parsing the YAML structure instead of using fragile regex matching.
118+ func parseAPILocaleEnum (path string ) ([]string , error ) {
119+ data , err := os .ReadFile (path )
120+ if err != nil {
121+ return nil , fmt .Errorf ("reading command-api.yaml: %w" , err )
122+ }
123+
124+ // Parse into a generic structure and navigate to the locale enum.
125+ var doc map [string ]interface {}
126+ if err := yaml .Unmarshal (data , & doc ); err != nil {
127+ return nil , fmt .Errorf ("parsing command-api.yaml: %w" , err )
128+ }
129+
130+ // Navigate: paths.*.*.properties.application.properties.locale.enum
131+ // The locale field is nested under application settings; find it by
132+ // walking all schema definitions looking for an "application" property
133+ // with a "locale" child that has an enum.
134+ locales := findLocaleEnum (doc )
135+ if locales == nil {
136+ return nil , fmt .Errorf ("could not find locale enum in command-api.yaml" )
137+ }
138+ return locales , nil
139+ }
140+
141+ // findLocaleEnum searches a parsed YAML structure for a "locale" property
142+ // definition that contains an "enum" list, returning the enum values.
143+ func findLocaleEnum (v interface {}) []string {
144+ switch node := v .(type ) {
145+ case map [string ]interface {}:
146+ // If this map has a "locale" key whose value has an "enum", that's it.
147+ if locale , found := node ["locale" ]; found {
148+ if localeMap , ok := locale .(map [string ]interface {}); ok {
149+ if enumVal , hasEnum := localeMap ["enum" ]; hasEnum {
150+ if items , ok := enumVal .([]interface {}); ok {
151+ var result []string
152+ for _ , item := range items {
153+ if s , ok := item .(string ); ok {
154+ result = append (result , s )
155+ }
156+ }
157+ if len (result ) > 0 {
158+ return result
159+ }
160+ }
161+ }
162+ }
163+ }
164+ // Recurse into all map values.
165+ for _ , val := range node {
166+ if result := findLocaleEnum (val ); result != nil {
167+ return result
168+ }
169+ }
170+ case []interface {}:
171+ for _ , val := range node {
172+ if result := findLocaleEnum (val ); result != nil {
173+ return result
174+ }
175+ }
176+ }
177+ return nil
178+ }
179+
180+ // specLocaleRe matches locale string values in test assertions, e.g.:
181+ //
182+ // { application: { locale: 'de' } }
183+ var specLocaleRe = regexp .MustCompile (`locale:\s*'([a-z][\w-]*)'` )
184+
185+ // crossValidateSpec checks that locale values used as valid inputs in
186+ // the settings validator spec are registered in the manifest.
187+ func crossValidateSpec (specContent string , manifestLocales map [string ]bool ) []string {
188+ matches := specLocaleRe .FindAllStringSubmatch (specContent , - 1 )
189+ seen := make (map [string ]bool )
190+ var errors []string
191+ for _ , m := range matches {
192+ code := m [1 ]
193+ if seen [code ] {
194+ continue
195+ }
196+ seen [code ] = true
197+ // "none" and "invalid" are special test values, not real locales.
198+ if code == "none" || code == "invalid" {
199+ continue
200+ }
201+ if ! manifestLocales [code ] {
202+ errors = append (errors , fmt .Sprintf (" settingsValidator.spec.ts uses locale %q not in manifest" , code ))
203+ }
204+ }
205+ return errors
206+ }
0 commit comments