者 | Hemanth Murali
譯者 | 張衛濱
策劃 | Tina
你是否已經涉足 Web 開發的汪洋大海?如果答案是肯定的,那你很快就會意識到,Web 不僅僅是為英語使用者服務的,它是面向全球的。假設法國用戶看到了一條令人困惑的純英文錯誤信息,在你被類似的投訴淹沒之前,我們先來討論一下什么是國際化(internationalization,通常簡寫為 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 方法確定的。這種方法雖然非常基礎,但是展示了根據用戶特定的本地語言提供內容的原則。但是,隨著內容的進展,我們將深入研究更先進的技術,并了解為何這種基礎的方式對于大型應用程序可能無法具備可擴展性和高效率。
在前一種方法的基礎之上,我們努力保留其優點,同時解決其缺點。為了實現這一點,我們將代碼庫中的硬編碼字符串值過渡到基于配置的設置。我們會為每種本地語言使用單獨的配置文件,并以 JSON 格式進行編碼。這種模塊化方式簡化了翻譯的添加和修改,無需進行代碼的變更。
如下是英語和西班牙語本地語言的配置文件:
文件名:en.json
{ "welcome_message": "Hello, World"}
復制代碼
文件名:es.json
{ "welcome_message": "Hola, Mundo"}
復制代碼
首先,我們需要一種讀取 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 方法使用標識符獲取所需的翻譯字符串。
緩解可能出現大型配置文件的一種方法是將其托管到內容分發網絡(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 上存儲配置文件并在需要時加載來緩解。第二個缺點可以通過在靜態字符串中使用占位符并在運行時根據上下文替換來解決。第三個缺點則需要一個健壯的錯誤處理機制和一些潛在的后備策略。
如果要翻譯的字符串有一部分內容是動態的,那么就需要一種更靈活的解決方案。以 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”都是動態元素。
我們來分析一下:
實現此類動態內容的一種方法是在配置文件中使用占位符,并在運行時根據上下文替換它們。
如下是一個 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