@@ -23,6 +23,8 @@ import (
2323 "fmt"
2424 "os"
2525 "path/filepath"
26+ "sort"
27+ "strings"
2628
2729 "github.com/google/wire"
2830 myTran "github.com/segmentfault/pacman/contrib/i18n"
@@ -100,6 +102,7 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
100102 // add translator use backend translation
101103 if err = myTran .AddTranslator (content , file .Name ()); err != nil {
102104 log .Debugf ("add translator failed: %s %s" , file .Name (), err )
105+ reportTranslatorFormatError (file .Name (), buf )
103106 continue
104107 }
105108 }
@@ -160,3 +163,165 @@ func TrWithData(lang i18n.Language, key string, templateData any) string {
160163 }
161164 return translation
162165}
166+
167+ // reportTranslatorFormatError re-parses the YAML file to locate the invalid entry
168+ // when go-i18n fails to add the translator.
169+ func reportTranslatorFormatError (fileName string , content []byte ) {
170+ var raw any
171+ if err := yaml .Unmarshal (content , & raw ); err != nil {
172+ log .Errorf ("parse translator file %s failed when diagnosing format error: %s" , fileName , err )
173+ return
174+ }
175+ if err := inspectTranslatorNode (raw , nil , true ); err != nil {
176+ log .Errorf ("translator file %s invalid: %s" , fileName , err )
177+ }
178+ }
179+
180+ func inspectTranslatorNode (node any , path []string , isRoot bool ) error {
181+ switch data := node .(type ) {
182+ case nil :
183+ if isRoot {
184+ return fmt .Errorf ("root value is empty" )
185+ }
186+ return fmt .Errorf ("%s contains an empty value" , formatTranslationPath (path ))
187+ case string :
188+ if isRoot {
189+ return fmt .Errorf ("root value must be an object but found string" )
190+ }
191+ return nil
192+ case bool , int , int8 , int16 , int32 , int64 , uint , uint8 , uint16 , uint32 , uint64 , float32 , float64 :
193+ if isRoot {
194+ return fmt .Errorf ("root value must be an object but found %T" , data )
195+ }
196+ return fmt .Errorf ("%s expects a string translation but found %T" , formatTranslationPath (path ), data )
197+ case map [string ]any :
198+ if isMessageMap (data ) {
199+ return nil
200+ }
201+ keys := make ([]string , 0 , len (data ))
202+ for key := range data {
203+ keys = append (keys , key )
204+ }
205+ sort .Strings (keys )
206+ for _ , key := range keys {
207+ if err := inspectTranslatorNode (data [key ], append (path , key ), false ); err != nil {
208+ return err
209+ }
210+ }
211+ return nil
212+ case map [string ]string :
213+ mapped := make (map [string ]any , len (data ))
214+ for k , v := range data {
215+ mapped [k ] = v
216+ }
217+ return inspectTranslatorNode (mapped , path , isRoot )
218+ case map [any ]any :
219+ if isMessageMap (data ) {
220+ return nil
221+ }
222+ type kv struct {
223+ key string
224+ val any
225+ }
226+ items := make ([]kv , 0 , len (data ))
227+ for key , val := range data {
228+ strKey , ok := key .(string )
229+ if ! ok {
230+ return fmt .Errorf ("%s uses a non-string key %#v" , formatTranslationPath (path ), key )
231+ }
232+ items = append (items , kv {key : strKey , val : val })
233+ }
234+ sort .Slice (items , func (i , j int ) bool {
235+ return items [i ].key < items [j ].key
236+ })
237+ for _ , item := range items {
238+ if err := inspectTranslatorNode (item .val , append (path , item .key ), false ); err != nil {
239+ return err
240+ }
241+ }
242+ return nil
243+ case []any :
244+ for idx , child := range data {
245+ nextPath := append (path , fmt .Sprintf ("[%d]" , idx ))
246+ if err := inspectTranslatorNode (child , nextPath , false ); err != nil {
247+ return err
248+ }
249+ }
250+ return nil
251+ case []map [string ]any :
252+ for idx , child := range data {
253+ nextPath := append (path , fmt .Sprintf ("[%d]" , idx ))
254+ if err := inspectTranslatorNode (child , nextPath , false ); err != nil {
255+ return err
256+ }
257+ }
258+ return nil
259+ default :
260+ if isRoot {
261+ return fmt .Errorf ("root value must be an object but found %T" , data )
262+ }
263+ return fmt .Errorf ("%s contains unsupported value type %T" , formatTranslationPath (path ), data )
264+ }
265+ }
266+
267+ var translatorReservedKeys = []string {
268+ "id" , "description" , "hash" , "leftdelim" , "rightdelim" ,
269+ "zero" , "one" , "two" , "few" , "many" , "other" ,
270+ }
271+
272+ func isMessageMap (data any ) bool {
273+ switch v := data .(type ) {
274+ case map [string ]any :
275+ for _ , key := range translatorReservedKeys {
276+ val , ok := v [key ]
277+ if ! ok {
278+ continue
279+ }
280+ if _ , ok := val .(string ); ok {
281+ return true
282+ }
283+ }
284+ case map [string ]string :
285+ for _ , key := range translatorReservedKeys {
286+ val , ok := v [key ]
287+ if ! ok {
288+ continue
289+ }
290+ if val != "" {
291+ return true
292+ }
293+ }
294+ case map [any ]any :
295+ for _ , key := range translatorReservedKeys {
296+ val , ok := v [key ]
297+ if ! ok {
298+ continue
299+ }
300+ if _ , ok := val .(string ); ok {
301+ return true
302+ }
303+ }
304+ }
305+ return false
306+ }
307+
308+ func formatTranslationPath (path []string ) string {
309+ if len (path ) == 0 {
310+ return "root"
311+ }
312+ var b strings.Builder
313+ for _ , part := range path {
314+ if part == "" {
315+ continue
316+ }
317+ if part [0 ] == '[' {
318+ b .WriteString (part )
319+ continue
320+ }
321+ if b .Len () > 0 {
322+ b .WriteByte ('.' )
323+ }
324+ b .WriteString (part )
325+ }
326+ return b .String ()
327+ }
0 commit comments