Line data Source code
1 : // Package middleware は、HTTPリクエストからロケールを交渉・取得するミドルウェアを提供します。
2 : package middleware
3 :
4 : import (
5 : "net/http"
6 : "strings"
7 :
8 : "github.com/gin-gonic/gin"
9 : )
10 :
11 : // LocaleSource は利用可能ロケールコードの列挙だけを要求する最小インターフェイスです。
12 : // 旧実装の Store がこのメソッドを持っていれば、そのまま渡せます。
13 : type LocaleSource interface {
14 : AvailableLocaleCodes() []string
15 : }
16 :
17 : // Opts はロケールネゴシエーションの設定です(旧Optsに準拠)。
18 : type Opts struct {
19 : Default string // 既定言語(例: "ja")
20 : Allow []string // 明示指定。空なら Store から自動検出
21 : Query string // 例: "lang"(空なら無効)
22 : Header string // 例: "Accept-Language"
23 : Cookie string // 例: "lang"(空なら無効)
24 : SetHeader bool // Content-Language を付与するか(既定: true)
25 : Store LocaleSource // 自動検出用(Allow が空のとき推奨)
26 : }
27 :
28 : // CtxLocaleKey は Gin Context に格納するキー名
29 : const CtxLocaleKey = "lang"
30 :
31 : // Middleware は、Header > Query > Cookie > Default の順にロケールを決定し、
32 : // Context にセットします(必要なら Response Header へ Content-Language も付与)。
33 2 : func Middleware(o Opts) gin.HandlerFunc {
34 3 : if o.Header == "" {
35 1 : o.Header = "Accept-Language"
36 1 : }
37 : // 旧実装同様、明示指定されなければ true
38 3 : if !o.SetHeader {
39 1 : o.SetHeader = true
40 1 : }
41 4 : return func(c *gin.Context) {
42 2 : loc := Negotiate(c.Request, o)
43 4 : if o.SetHeader {
44 2 : c.Writer.Header().Set("Content-Language", loc)
45 2 : }
46 2 : c.Set(CtxLocaleKey, loc)
47 2 : c.Next()
48 : }
49 : }
50 :
51 : // From は Context からロケールを取り出すヘルパーです。
52 1 : func From(c *gin.Context) string {
53 2 : if v, ok := c.Get(CtxLocaleKey); ok {
54 2 : if s, ok := v.(string); ok {
55 1 : return s
56 1 : }
57 : }
58 0 : return ""
59 : }
60 :
61 : // Negotiate は HTTP リクエストからロケールを決定して返します。
62 : // 優先順位:Query > Header > Cookie > Default
63 2 : func Negotiate(r *http.Request, o Opts) string {
64 3 : inAllow := func(code string) bool {
65 1 : code = strings.ToLower(strings.TrimSpace(code))
66 1 : if code == "" {
67 0 : return false
68 0 : }
69 : // Allow 未指定なら Store から検出
70 1 : allow := o.Allow
71 1 : if len(allow) == 0 && o.Store != nil {
72 0 : allow = o.Store.AvailableLocaleCodes()
73 0 : }
74 1 : if len(allow) == 0 {
75 0 : return true // 制限なし
76 0 : }
77 2 : for _, a := range allow {
78 2 : if strings.EqualFold(a, code) {
79 1 : return true
80 1 : }
81 : }
82 1 : return false
83 : }
84 :
85 : // 1) Query
86 4 : if qk := strings.TrimSpace(o.Query); qk != "" {
87 3 : if v := strings.TrimSpace(r.URL.Query().Get(qk)); v != "" && inAllow(v) {
88 1 : return strings.ToLower(v)
89 1 : }
90 : }
91 :
92 : // 2) Header(先頭、地域は落として base を試す)
93 4 : if o.Header != "" {
94 2 : al := r.Header.Get(o.Header)
95 3 : if al != "" {
96 2 : for _, part := range strings.Split(al, ",") {
97 1 : raw := strings.TrimSpace(strings.Split(part, ";")[0])
98 1 : if raw == "" {
99 0 : continue
100 : }
101 1 : raw = strings.ToLower(raw)
102 1 : // そのまま
103 1 : if inAllow(raw) {
104 0 : return raw
105 0 : }
106 : // サブタグ切り落とし(例: en-US -> en)
107 2 : if i := strings.IndexByte(raw, '-'); i > 0 {
108 1 : base := raw[:i]
109 2 : if inAllow(base) {
110 1 : return base
111 1 : }
112 : }
113 : }
114 : }
115 : }
116 :
117 : // 3) Cookie
118 2 : if ck := strings.TrimSpace(o.Cookie); ck != "" {
119 0 : if c, err := r.Cookie(ck); err == nil && c != nil {
120 0 : if v := strings.TrimSpace(c.Value); v != "" && inAllow(v) {
121 0 : return strings.ToLower(v)
122 0 : }
123 : }
124 : }
125 :
126 : // 4) Default(最終フォールバック)
127 2 : return strings.ToLower(strings.TrimSpace(o.Default))
128 : }
129 :
130 : // Setter は Cookie を使ってロケールを保存するためのハンドラです。
131 : // 例: GET /lang?to=en
132 0 : func Setter(o Opts) gin.HandlerFunc {
133 0 : return func(c *gin.Context) {
134 0 : to := strings.TrimSpace(c.Query("to"))
135 0 : if to == "" {
136 0 : to = o.Default
137 0 : }
138 0 : http.SetCookie(c.Writer, &http.Cookie{
139 0 : Name: o.Cookie,
140 0 : Value: to,
141 0 : Path: "/",
142 0 : HttpOnly: true,
143 0 : MaxAge: 86400 * 365,
144 0 : })
145 0 : c.JSON(http.StatusOK, gin.H{"ok": true, "locale": to})
146 : }
147 : }
|