欧美vvv,亚洲第一成人在线,亚洲成人欧美日韩在线观看,日本猛少妇猛色XXXXX猛叫

新聞資訊

    者 | Hemanth Murali

    譯者 | 張衛濱

    策劃 | Tina


    快速閱讀


    • 國際化(i18n)和本地化是 Web 開發中的關鍵流程,能夠確保軟件適用于不同的語言和地區,并確保軟件實際適配這些特定的需求。
    • 盡管以 JavaScript 為核心的 i18n 庫(如 i18next、react-intl 和 react-i18next)是該領域的主流工具,可幫助開發人員高效地處理翻譯和本地化相關的配置,但它們僅適用于基于 JavaScript 的 Web 應用。我們需要一個與語言無關的國際化框架。
    • JSON 是一種廣泛接受的格式,可用于存儲翻譯和本地化相關的配置,無論使用何種語言和框架,都能在各種應用程序中輕松集成和動態替換內容。
    • 內容分發網絡(Content Delivery Network,CDN)可被戰略性地用于高效提供本地化相關的配置文件,從而減輕加載大型配置文件潛在的弊端。
    • 構建自定義國際化框架,并將其與數據庫或數據存儲解決方案集成,可以實現動態和上下文感知的翻譯,從而增強不同地區和語言的用戶體驗。


    你是否已經涉足 Web 開發的汪洋大海?如果答案是肯定的,那你很快就會意識到,Web 不僅僅是為英語使用者服務的,它是面向全球的。假設法國用戶看到了一條令人困惑的純英文錯誤信息,在你被類似的投訴淹沒之前,我們先來討論一下什么是國際化(internationalization,通常簡寫為 i18n)和本地化。


    i18n 這個流行詞是什么意思?


    想象一下,在這個世界上,無論每個人的母語是什么,你的軟件都可以與他們流暢地交流。這就是國際化和本地化要實現的目標。雖然乍看上去沒啥特別之處,但是請記住,本地化應用程序不僅僅是翻譯文本。而是要根據用戶的文化、地區和語言偏好提供量身定制的體驗。


    但是,這里有個障礙在等著你。深入了解 i18n 庫的工具箱,你會發現以 JavaScript 為核心的解決方案占據了主導地位,尤其是那些圍繞 React 的解決方案(如 i18next、react-intl 和 react-i18next )。


    如果跳出 JavaScript 的范疇,可選的方案就會越來越少。更糟糕的是,這些現成的工具通常都帶有“一刀切”的特點,缺乏適配特定用例的能力。


    不過,不必擔心!如果鞋子不合適的話,為何不自己動手做呢?請繼續往下閱讀,我們將指導你從頭開始構建一個國際化框架:一個為你的應用程序量身定制、跨語言和跨框架的解決方案。


    準備好為你的應用程序簽發全球通行證了嗎?讓我們開始這段旅程吧。


    基礎的方式


    掌握國際化精髓的一個簡單方法就是使用一個函數,該函數能夠根據用戶所在的地域獲取信息。如下是一個使用 Java 編寫的樣例,它提供了一個基本但有效的方法:


    public class InternationalizationExample {
    
    
        public static void main(String[] args) {
            System.out.println(getWelcomeMessage(getUserLocale()));
        }
    
    
        public static String getWelcomeMessage(String locale) {
            switch (locale) {
                case "en_US":
                    return "Hello, World!";
                case "fr_FR":
                    return "Bonjour le Monde!";
                case "es_ES":
                    return "Hola Mundo!";
                default:
                    return "Hello, World!";
            }
        }
    
    
        public static String getUserLocale() {
            // This is a placeholder method. In a real-world scenario,
            // you'd fetch the user's locale from their settings or system configuration.
            return "en_US";  // This is just an example.
        }
    }

    復制代碼


    在上面的樣例中,getWelcomeMessage 根據 locale 指定的語言返回歡迎信息。語言是由 getUserLocale 方法確定的。這種方法雖然非常基礎,但是展示了根據用戶特定的本地語言提供內容的原則。但是,隨著內容的進展,我們將深入研究更先進的技術,并了解為何這種基礎的方式對于大型應用程序可能無法具備可擴展性和高效率。


    優點


    • 覆蓋面廣:由于所有的翻譯都嵌入在代碼中,因此我們可以使用多種語言,而不必擔心外部依賴或缺失翻譯。
    • 無網絡調用:翻譯直接從代碼中獲取,無需任何網絡開銷或從外部源獲取翻譯相關的延遲。
    • 便利的代碼搜索:由于所有的翻譯都是源碼的一部分,因此搜索特定翻譯或排查相關的問題變得很簡單易行。
    • 可讀性:開發人員可以立即理解選擇特定翻譯背后的流程和邏輯,從而簡化調試和維護。
    • 減少外部依賴:無需依賴外部翻譯服務或數據庫,這意味著應用程序中少了一個故障點。


    缺點:


    • 更新操作需要發布新的版本:在移動應用或獨立應用的場景中,添加新語言或調整現有的翻譯需要用戶下載并更新最新版本的應用。
    • 冗余代碼:隨著要支持語言數量的增加,switch 和條件語句也會相應地增加,從而導致代碼的重復和臃腫。
    • 合并沖突:由于多個開發人員可能會對各種語言進行添加或修改,所以版本控制系統中出現合并沖突的風險會隨之增加。
    • 代碼維護所面臨的挑戰:隨著時間的推移,應用程序會進行擴展并支持更多的本地語言,直接在代碼中管理和更新翻譯會變得繁瑣且容易出錯。
    • 靈活性有限:采用這種靜態的方式很難添加像復數形式、特定上下文的翻譯或動態獲取翻譯等特性。
    • 性能開銷:對于大規模應用而言,加載大塊的翻譯數據卻僅使用其中很小的一部分會導致資源緊張,造成效率低下。


    基于配置的國際化


    在前一種方法的基礎之上,我們努力保留其優點,同時解決其缺點。為了實現這一點,我們將代碼庫中的硬編碼字符串值過渡到基于配置的設置。我們會為每種本地語言使用單獨的配置文件,并以 JSON 格式進行編碼。這種模塊化方式簡化了翻譯的添加和修改,無需進行代碼的變更。


    如下是英語和西班牙語本地語言的配置文件:


    文件名:en.json


    {    "welcome_message": "Hello, World"}
    
    

    復制代碼


    文件名:es.json


    {    "welcome_message": "Hola, Mundo"}
    
    

    復制代碼


    Java 中的實現:


    首先,我們需要一種讀取 JSON 文件的方式。這通常會使用像 Jackson 或 GSON 這樣的庫。在本例中,我們將使用 Jackson。


    import com.fasterxml.jackson.databind.ObjectMapper;
    import java.io.File;
    import java.io.IOException;
    import java.util.Map;
    
    
    public class Internationalization {
    
    
        private static final String CONFIG_PATH = "/path_to_configs/";
        private Map<String, String> translations;
    
    
        public Internationalization(String locale) throws IOException {
            ObjectMapper mapper = new ObjectMapper();
            translations = mapper.readValue(new File(CONFIG_PATH + locale + ".json"), Map.class);
        }
    
    
        public String getTranslation(String key) {
            return translations.getOrDefault(key, "Key not found!");
        }
    }
    
    
    public static class Program {
    
    
        public static void main(String[] args) throws IOException {
            Internationalization i18n = new Internationalization(getUserLocale());
            System.out.println(i18n.getTranslation("welcome_message"));
        }
    
    
        private static String getUserLocale() {
            // This method should be implemented to fetch the user's locale.
            // For now, let's just return "en" for simplicity.
            return "en";
        }
    }

    復制代碼


    Internationalization 類在實例化的時候,會根據提供的本地語言讀取上述代碼中相關的 JSON 配置。getTranslation 方法使用標識符獲取所需的翻譯字符串。


    優點:


    • 保留了上述方式的所有優點:覆蓋面廣,加載后無需使用網絡就能進行翻譯,代碼易于搜索和閱讀。
    • 動態加載:可根據用戶的本地語言動態加載翻譯。只需加載必要的翻譯,從而帶來潛在的性能優勢。
    • 可擴展性:添加新語言更容易。只需為該語言添加一個新的配置文件,應用程序就能處理它,無需任何代碼修改。
    • 更整潔的代碼:邏輯與翻譯分離,代碼更簡潔、更易維護。
    • 中心化的管理:所有的翻譯都集中在一個文件中,因此更易于管理、審查和更新。這種方法提供了一種更可擴展、更簡潔的國際化處理方式,尤其適用于大型應用程序。

    缺點:


    • 可能會導致配置文件過大:隨著應用程序的增長和對多種語言的支持,這些配置文件可能會變得相當大。這可能會導致應用程序的初始加載出現滯后,尤其是在配置文件需要前期加載的情況中。


    從 CDN 抓取配置


    緩解可能出現大型配置文件的一種方法是將其托管到內容分發網絡(Content Delivery Network,CDN)上。通過這種方式,應用程序可以根據用戶的本地語言只加載必要的配置文件。這樣既能保證應用程序的運行速度,又能減少用戶不必要下載的數據量。當用戶切換本地語言或探測到不同的本地語言時,可以根據需要從 CDN 獲取配置。這為大規模應用程序提供了速度和靈活性之間的最佳平衡。為了簡單起見,我們考慮使用基礎的 HTTP 庫來獲取配置文件。在這個 Java 樣例中,我們將使用虛構的 HttpUtil 庫:


    import java.util.Map;
    import org.json.JSONObject;
    
    
    public class InternationalizationService {
    
    
        private static final String CDN_BASE_URL = "https://cdn.example.com/locales/";
    
    
        public String getTranslatedString(String key) {
            String locale = getUserLocale();
            String configContent = fetchConfigFromCDN(locale);
            JSONObject configJson = new JSONObject(configContent);
            return configJson.optString(key, "Translation not found");
        }
    
    
        private String fetchConfigFromCDN(String locale) {
            String url = CDN_BASE_URL + locale + ".json";
            return HttpUtil.get(url);  // Assuming this method fetches content from a given URL
        }
    
    
        private String getUserLocale() {
            // Implement method to get the user's locale
            // This can be fetched from user preferences, system settings, etc.
            return "en";  // Defaulting to English for this example
        }
    }

    復制代碼


    注意:上述代碼只是一個簡化的樣例,在實際的場景中可能需要錯誤處理、緩存機制和其他優化。


    這里的想法是根據用戶的本地語言直接從 CDN 獲取必要的配置文件。用戶的本地語言決定了配置文件的 URL,獲取到之后,就會對配置文件進行解析,以獲得所需的翻譯。如果找不到相應地鍵,就會返回默認信息。這種方法的好處是,應用程序只需加載必要的翻譯,從而確保了最佳性能。


    優點


    • 繼承了前一種方法的所有優勢。
    • 易于為新的本地語言組織和添加翻譯。
    • 只需獲取必要的翻譯,因此加載效率高。


    缺點:


    • 配置文件體積龐大,可能會降低應用程序的初始化速度。
    • 字符串必須是靜態的。無法直接支持動態字符串或需要運行時計算的字符串。如果需要在翻譯中插入動態數據,這可能是一個限制。
    • 依賴外部服務(CDN)。如果 CDN 遇到故障或出現問題,應用程序將無法獲取翻譯內容。


    但是,要解決這些缺點,我們可以采取如下措施:第一個缺點可以通過在 CDN 上存儲配置文件并在需要時加載來緩解。第二個缺點可以通過在靜態字符串中使用占位符并在運行時根據上下文替換來解決。第三個缺點則需要一個健壯的錯誤處理機制和一些潛在的后備策略。


    動態字符串處理


    如果要翻譯的字符串有一部分內容是動態的,那么就需要一種更靈活的解決方案。以 Facebook 為例,在 News Feed 中,我們會看到這里使用了自定義的字符串來表示每篇文章的“Likes”信息。比如,如果文章只有一個“Likes”信息,那么你可能會看到“John likes your post.”。如果有兩個“Likes”信息,那么你可能會看到“John and David like your post.”。如果有兩個以上的“Likes”信息,你可能會看到“John, David and 100 others like your post.”。在這種情況下,需要進行一些自定義。動詞“like”和“likes”是根據喜歡文章的人數來確定的。如何做到這一點呢?


    考慮如下的樣例:“John, David and 100 other people recently reacted to your post.”,在這里“David”、“John”、“100”、“people”和“reacted”都是動態元素。


    我們來分析一下:


    • “David”和“John”可以是從與用戶相關的方法或數據庫中獲取的用戶名。
    • “100”可以是從與文章相關的方法或數據庫中獲取的對文章做出反應的總人數,其中不包括 David 和 John。
    • 當代指一個集體時,“people”可以是名詞“人”的復數形式。
    • “reacted”可用于用戶以愛心、關注或憤怒等圖標對文章做出反應,而不能是表示喜歡的圖標。


    實現此類動態內容的一種方法是在配置文件中使用占位符,并在運行時根據上下文替換它們。


    如下是一個 Java 樣例:


    配置文件(適用于英語)


    {
          oneUserAction: {0} {1} your post,
          twoUserAction: {0} and {1} {2} your post,
          multiUserAction: {0}, {1} and {2} other {3} recently {4} to your post,
          people: people,
          likeSingular: likes,
          likePlural: like,
    }

    復制代碼


    配置文件(適用于法語):


    {
          oneUserAction: {0} {1} votre publication,
          twoUserAction: {0} et {1} {2} votre publication,
          multiUserAction: {0}, {1} et {2} autres {3} ont récemment {4} à votre publication,
          people: personnes,
          likeSingular: aime,
          likePlural: aiment,
    }

    復制代碼


    Java 實現:


    import java.util.Locale;
    import java.util.ResourceBundle;
    
    
    public class InternationalizationExample {
    
    
        public static void main(String[] args) {
            // Examples
            System.out.println(createMessage("David", null, 1, new Locale("en", "US"))); // One user
            System.out.println(createMessage("David", "John", 2, new Locale("en", "US"))); // Two users
            System.out.println(createMessage("David", "John", 100, new Locale("en", "US"))); // Multiple users
    
    
            // French examples
            System.out.println(createMessage("David", null, 1, new Locale("fr", "FR"))); // One user
            System.out.println(createMessage("David", "John", 2, new Locale("fr", "FR"))); // Two users
            System.out.println(createMessage("David", "John", 100, new Locale("fr", "FR"))); // Multiple users
        }
    
    
        private static String createMessage(String user1, String user2, int count, Locale locale) {
            // Load the appropriate resource bundle
            ResourceBundle messages = ResourceBundle.getBundle("MessagesBundle", locale);    
    
    
            if (count == 0) {
                return ""; // No likes received
            } else if (count == 1) {
                return String.format(
                      messages.getString("oneUserAction"),
                      user1,
                      messages.getString("likeSingular")
                ); // For one like, returns "David likes your post"
            } else if (count == 2) {
                return String.format(
                      messages.getString("twoUserAction"),
                      user1,
                      user2,
                      messages.getString("likePlural")
                ); // For two likes, returns "David and John like your post"
            } else {
                return String.format(
                      messages.getString("multiUserAction"),
                      user1,
                      user2,
                      count,
                      messages.getString("people"),
                      messages.getString("likePlural")
                      ); // For more than two likes, returns "David, John and 100 other people like your post"
            }
        }
    }

    復制代碼


    結論


    無論規模大小,開發有效的國際化(i18n)和本地化(l10n)框架對于軟件應用都至關重要。這種方法可以確保你的應用能夠與用戶的母語和文化背景產生共鳴。雖然字符串翻譯是 i18n 和 l10n 的一個重要組成部分,但它只是軟件全球化這一更廣泛挑戰的一個方面而已。


    有效的本地化不僅僅是翻譯,還要解決其他的關鍵問題,例如書寫方向,阿拉伯語等語言的書寫方向(從右到左)和文本長度或大小各不相同,泰米爾語等語言的文字可能比英語更長。通過精心定制這些策略來滿足特定的本地化需求,你就可以為軟件提供真正全球化的、適用于不同文化的用戶體驗。

    原文鏈接:構建國際化框架,Web開發讓語言無阻_框架_InfoQ精選文章

    以下文章來源于李文周 ,作者李文周

    validator庫實用技巧

    在web開發中一個不可避免的環節就是對請求參數進行校驗,通常我們會在代碼中定義與請求參數相對應的模型(結構體),借助模型綁定快捷地解析請求中的參數,例如 gin 框架中的Bind和ShouldBind系列方法。本文就以 gin 框架的請求參數校驗為例,介紹一些validator庫的實用技巧。

    gin框架使用github.com/go-playground/validator進行參數校驗,目前已經支持github.com/go-playground/validator/v10了,我們需要在定義結構體時使用 binding tag標識相關校驗規則,可以查看validator文檔查看支持的所有 tag。

    基本示例

    首先來看gin框架內置使用validator做參數校驗的基本示例。

    package main
    
    import (
     "net/http"
    
     "github.com/gin-gonic/gin"
    )
    
    type SignUpParam struct {
     Age        uint8  `json:"age" binding:"gte=1,lte=130"`
     Name       string `json:"name" binding:"required"`
     Email      string `json:"email" binding:"required,email"`
     Password   string `json:"password" binding:"required"`
     RePassword string `json:"re_password" binding:"required,eqfield=Password"`
    }
    
    func main() {
     r := gin.Default()
    
     r.POST("/signup", func(c *gin.Context) {
      var u SignUpParam
      if err := c.ShouldBind(&u); err != nil {
       c.JSON(http.StatusOK, gin.H{
        "msg": err.Error(),
       })
       return
      }
      // 保存入庫等業務邏輯代碼...
    
      c.JSON(http.StatusOK, "success")
     })
    
     _ = r.Run(":8999")
    }

    我們使用curl發送一個POST請求測試下:

    curl -H "Content-type: application/json" -X POST -d '{"name":"q1mi","age":18,"email":"123.com"}' http://127.0.0.1:8999/signup

    輸出結果:

    {"msg":"Key: 'SignUpParam.Email' Error:Field validation for 'Email' failed on the 'email' tag\nKey: 'SignUpParam.Password' Error:Field validation for 'Password' failed on the 'required' tag\nKey: 'SignUpParam.RePassword' Error:Field validation for 'RePassword' failed on the 'required' tag"}

    從最終的輸出結果可以看到 validator 的檢驗生效了,但是錯誤提示的字段不是特別友好,我們可能需要將它翻譯成中文。

    翻譯校驗錯誤提示信息

    validator庫本身是支持國際化的,借助相應的語言包可以實現校驗錯誤提示信息的自動翻譯。下面的示例代碼演示了如何將錯誤提示信息翻譯成中文,翻譯成其他語言的方法類似。

    package main
    
    import (
     "fmt"
     "net/http"
    
     "github.com/gin-gonic/gin"
     "github.com/gin-gonic/gin/binding"
     "github.com/go-playground/locales/en"
     "github.com/go-playground/locales/zh"
     ut "github.com/go-playground/universal-translator"
     "github.com/go-playground/validator/v10"
     enTranslations "github.com/go-playground/validator/v10/translations/en"
     zhTranslations "github.com/go-playground/validator/v10/translations/zh"
    )
    
    // 定義一個全局翻譯器T
    var trans ut.Translator
    
    // InitTrans 初始化翻譯器
    func InitTrans(locale string) (err error) {
     // 修改gin框架中的Validator引擎屬性,實現自定制
     if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    
      zhT := zh.New() // 中文翻譯器
      enT := en.New() // 英文翻譯器
    
      // 第一個參數是備用(fallback)的語言環境
      // 后面的參數是應該支持的語言環境(支持多個)
      // uni := ut.New(zhT, zhT) 也是可以的
      uni := ut.New(enT, zhT, enT)
    
      // locale 通常取決于 http 請求頭的 'Accept-Language'
      var ok bool
      // 也可以使用 uni.FindTranslator(...) 傳入多個locale進行查找
      trans, ok = uni.GetTranslator(locale)
      if !ok {
       return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
      }
    
      // 注冊翻譯器
      switch locale {
      case "en":
       err = enTranslations.RegisterDefaultTranslations(v, trans)
      case "zh":
       err = zhTranslations.RegisterDefaultTranslations(v, trans)
      default:
       err = enTranslations.RegisterDefaultTranslations(v, trans)
      }
      return
     }
     return
    }
    
    type SignUpParam struct {
     Age        uint8  `json:"age" binding:"gte=1,lte=130"`
     Name       string `json:"name" binding:"required"`
     Email      string `json:"email" binding:"required,email"`
     Password   string `json:"password" binding:"required"`
     RePassword string `json:"re_password" binding:"required,eqfield=Password"`
    }
    
    func main() {
     if err := InitTrans("zh"); err != nil {
      fmt.Printf("init trans failed, err:%v\n", err)
      return
     }
    
     r := gin.Default()
    
     r.POST("/signup", func(c *gin.Context) {
      var u SignUpParam
      if err := c.ShouldBind(&u); err != nil {
       // 獲取validator.ValidationErrors類型的errors
       errs, ok := err.(validator.ValidationErrors)
       if !ok {
        // 非validator.ValidationErrors類型錯誤直接返回
        c.JSON(http.StatusOK, gin.H{
         "msg": err.Error(),
        })
        return
       }
       // validator.ValidationErrors類型錯誤則進行翻譯
       c.JSON(http.StatusOK, gin.H{
        "msg":errs.Translate(trans),
       })
       return
      }
      // 保存入庫等具體業務邏輯代碼...
    
      c.JSON(http.StatusOK, "success")
     })
    
     _ = r.Run(":8999")
    }

    同樣的請求再來一次:

    curl -H "Content-type: application/json" -X POST -d '{"name":"q1mi","age":18,"email":"123.com"}' http://127.0.0.1:8999/signup

    這一次的輸出結果如下:

    {"msg":{"SignUpParam.Email":"Email必須是一個有效的郵箱","SignUpParam.Password":"Password為必填字段","SignUpParam.RePassword":"RePassword為必填字段"}}

    自定義錯誤提示信息的字段名

    上面的錯誤提示看起來是可以了,但是還是差點意思,首先是錯誤提示中的字段并不是請求中使用的字段,例如:RePassword是我們后端定義的結構體中的字段名,而請求中使用的是re_password字段。如何是錯誤提示中的字段使用自定義的名稱,例如jsontag指定的值呢?

    只需要在初始化翻譯器的時候像下面一樣添加一個獲取json tag的自定義方法即可。

    // InitTrans 初始化翻譯器
    func InitTrans(locale string) (err error) {
     // 修改gin框架中的Validator引擎屬性,實現自定制
     if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    
      // 注冊一個獲取json tag的自定義方法
      v.RegisterTagNameFunc(func(fld reflect.StructField) string {
       name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
       if name == "-" {
        return ""
       }
       return name
      })
    
      zhT := zh.New() // 中文翻譯器
      enT := en.New() // 英文翻譯器
    
      // 第一個參數是備用(fallback)的語言環境
      // 后面的參數是應該支持的語言環境(支持多個)
      // uni := ut.New(zhT, zhT) 也是可以的
      uni := ut.New(enT, zhT, enT)
    
      // ... liwenzhou.com ...
    }

    再嘗試發請求,看一下效果:

    {"msg":{"SignUpParam.email":"email必須是一個有效的郵箱","SignUpParam.password":"password為必填字段","SignUpParam.re_password":"re_password為必填字段"}}

    可以看到現在錯誤提示信息中使用的就是我們結構體中jsontag設置的名稱了。

    但是還是有點瑕疵,那就是最終的錯誤提示信息中心還是有我們后端定義的結構體名稱——SignUpParam,這個名稱其實是不需要隨錯誤提示返回給前端的,前端并不需要這個值。我們需要想辦法把它去掉。

    這里參考https://github.com/go-playground/validator/issues/633#issuecomment-654382345提供的方法,定義一個去掉結構體名稱前綴的自定義方法:

    func removeTopStruct(fields map[string]string) map[string]string {
     res := map[string]string{}
     for field, err := range fields {
      res[field[strings.Index(field, ".")+1:]] = err
     }
     return res
    }

    我們在代碼中使用上述函數將翻譯后的errors做一下處理即可:

    if err := c.ShouldBind(&u); err != nil {
     // 獲取validator.ValidationErrors類型的errors
     errs, ok := err.(validator.ValidationErrors)
     if !ok {
      // 非validator.ValidationErrors類型錯誤直接返回
      c.JSON(http.StatusOK, gin.H{
       "msg": err.Error(),
      })
      return
     }
     // validator.ValidationErrors類型錯誤則進行翻譯
     // 并使用removeTopStruct函數去除字段名中的結構體名稱標識
     c.JSON(http.StatusOK, gin.H{
      "msg": removeTopStruct(errs.Translate(trans)),
     })
     return
    }

    看一下最終的效果:

    {"msg":{"email":"email必須是一個有效的郵箱","password":"password為必填字段","re_password":"re_password為必填字段"}}

    這一次看起來就比較符合我們預期的標準了。

    自定義結構體校驗方法

    上面的校驗還是有點小問題,就是當涉及到一些復雜的校驗規則,比如re_password字段需要與password字段的值相等這樣的校驗規則,我們的自定義錯誤提示字段名稱方法就不能很好解決錯誤提示信息中的其他字段名稱了。

    curl -H "Content-type: application/json" -X POST -d '{"name":"q1mi","age":18,"email":"123.com","password":"123","re_password":"321"}' http://127.0.0.1:8999/signup

    最后輸出的錯誤提示信息如下:

    {"msg":{"email":"email必須是一個有效的郵箱","re_password":"re_password必須等于Password"}}

    可以看到re_password字段的提示信息中還是出現了Password這個結構體字段名稱。這有點小小的遺憾,畢竟自定義字段名稱的方法不能影響被當成param傳入的值。

    此時如果想要追求更好的提示效果,將上面的Password字段也改為和json tag一致的名稱,就需要我們自定義結構體校驗的方法。

    例如,我們為SignUpParam自定義一個校驗方法如下:

    // SignUpParamStructLevelValidation 自定義SignUpParam結構體校驗函數
    func SignUpParamStructLevelValidation(sl validator.StructLevel) {
     su := sl.Current().Interface().(SignUpParam)
    
     if su.Password != su.RePassword {
      // 輸出錯誤提示信息,最后一個參數就是傳遞的param
      sl.ReportError(su.RePassword, "re_password", "RePassword", "eqfield", "password")
     }
    }

    然后在初始化校驗器的函數中注冊該自定義校驗方法即可:

    func InitTrans(locale string) (err error) {
     // 修改gin框架中的Validator引擎屬性,實現自定制
     if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    
      // ... liwenzhou.com ...
        
      // 為SignUpParam注冊自定義校驗方法
      v.RegisterStructValidation(SignUpParamStructLevelValidation, SignUpParam{})
    
      zhT := zh.New() // 中文翻譯器
      enT := en.New() // 英文翻譯器
    
      // ... liwenzhou.com ...
    }

    最終再請求一次,看一下效果:

    {"msg":{"email":"email必須是一個有效的郵箱","re_password":"re_password必須等于password"}}

    這一次re_password字段的錯誤提示信息就符合我們預期了。

    自定義字段校驗方法

    除了上面介紹到的自定義結構體校驗方法,validator還支持為某個字段自定義校驗方法,并使用RegisterValidation()注冊到校驗器實例中。

    接下來我們來為SignUpParam添加一個需要使用自定義校驗方法checkDate做參數校驗的字段Date。

    type SignUpParam struct {
     Age        uint8  `json:"age" binding:"gte=1,lte=130"`
     Name       string `json:"name" binding:"required"`
     Email      string `json:"email" binding:"required,email"`
     Password   string `json:"password" binding:"required"`
     RePassword string `json:"re_password" binding:"required,eqfield=Password"`
     // 需要使用自定義校驗方法checkDate做參數校驗的字段Date
     Date       string `json:"date" binding:"required,datetime=2006-01-02,checkDate"`
    }

    其中datetime=2006-01-02是內置的用于校驗日期類參數是否滿足指定格式要求的tag。 如果傳入的date參數不滿足2006-01-02這種格式就會提示如下錯誤:

    {"msg":{"date":"date的格式必須是2006-01-02"}}

    針對date字段除了內置的datetime=2006-01-02提供的格式要求外,假設我們還要求該字段的時間必須是一個未來的時間(晚于當前時間),像這樣針對某個字段的特殊校驗需求就需要我們使用自定義字段校驗方法了。

    首先我們要在需要執行自定義校驗的字段后面添加自定義tag,這里使用的是checkDate,注意使用英文分號分隔開。

    // customFunc 自定義字段級別校驗方法
    func customFunc(fl validator.FieldLevel) bool {
     date, err := time.Parse("2006-01-02", fl.Field().String())
     if err != nil {
      return false
     }
     if date.Before(time.Now()) {
      return false
     }
     return true
    }

    定義好了字段及其自定義校驗方法后,就需要將它們聯系起來并注冊到我們的校驗器實例中。

    // 在校驗器注冊自定義的校驗方法
    if err := v.RegisterValidation("checkDate", customFunc); err != nil {
     return err
    }

    這樣,我們就可以對請求參數中date字段執行自定義的checkDate進行校驗了。 我們發送如下請求測試一下:

    curl -H "Content-type: application/json" -X POST -d '{"name":"q1mi","age":18,"email":"123@qq.com","password":"123", "re_password": "123", "date":"2020-01-02"}' http://127.0.0.1:8999/signup

    此時得到的響應結果是:

    {"msg":{"date":"Key: 'SignUpParam.date' Error:Field validation for 'date' failed on the 'checkDate' tag"}}

    這...自定義字段級別的校驗方法的錯誤提示信息很“簡單粗暴”,和我們上面的中文提示風格有出入,必須想辦法搞定它呀!

    自定義翻譯方法

    我們現在需要為自定義字段校驗方法提供一個自定義的翻譯方法,從而實現該字段錯誤提示信息的自定義顯示。

    // registerTranslator 為自定義字段添加翻譯功能
    func registerTranslator(tag string, msg string) validator.RegisterTranslationsFunc {
     return func(trans ut.Translator) error {
      if err := trans.Add(tag, msg, false); err != nil {
       return err
      }
      return nil
     }
    }
    
    // translate 自定義字段的翻譯方法
    func translate(trans ut.Translator, fe validator.FieldError) string {
     msg, err := trans.T(fe.Tag(), fe.Field())
     if err != nil {
      panic(fe.(error).Error())
     }
     return msg
    }

    定義好了相關翻譯方法之后,我們在InitTrans函數中通過調用RegisterTranslation()方法來注冊我們自定義的翻譯方法。

    // InitTrans 初始化翻譯器
    func InitTrans(locale string) (err error) {
     // ...liwenzhou.com...
     
      // 注冊翻譯器
      switch locale {
      case "en":
       err = enTranslations.RegisterDefaultTranslations(v, trans)
      case "zh":
       err = zhTranslations.RegisterDefaultTranslations(v, trans)
      default:
       err = enTranslations.RegisterDefaultTranslations(v, trans)
      }
      if err != nil {
       return err
      }
      // 注意!因為這里會使用到trans實例
      // 所以這一步注冊要放到trans初始化的后面
      if err := v.RegisterTranslation(
       "checkDate",
       trans,
       registerTranslator("checkDate", "{0}必須要晚于當前日期"),
       translate,
      ); err != nil {
       return err
      }
      return
     }
     return
    }

    這樣再次嘗試發送請求,就能得到想要的錯誤提示信息了。

    {"msg":{"date":"date必須要晚于當前日期"}}
    {"msg":{"date":"date必須要晚于當前日期"}}

    總結

    由于本篇博客示例代碼較多,我已經把文中示例代碼上傳到我的github倉庫——https://github.com/Q1mi/validator_demo,大家可以查看完整的示例代碼。

    本文總結的gin框架中validator的使用技巧同樣也適用于直接使用validator庫,區別僅僅在于我們配置的是gin框架中的校驗器還是由validator.New()創建的校驗器。同時使用validator庫確實能夠在一定程度上減少我們的編碼量,但是它不太可能完美解決我們所有需求,所以你需要找到兩者之間的平衡點。

    參考鏈接:

    https://github.com/go-playground/validator/blob/master/_examples/simple/main.go

    https://github.com/go-playground/validator/blob/master/_examples/translations/main.go

    https://github.com/go-playground/validator/issues/567

    https://github.com/go-playground/validator/issues/633

    https://github.com/go-playground/validator/issues/551

網站首頁   |    關于我們   |    公司新聞   |    產品方案   |    用戶案例   |    售后服務   |    合作伙伴   |    人才招聘   |   

友情鏈接: 餐飲加盟

地址:北京市海淀區    電話:010-     郵箱:@126.com

備案號:冀ICP備2024067069號-3 北京科技有限公司版權所有