summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Elliott <james-d-elliott@users.noreply.github.com>2022-09-16 11:19:16 +1000
committerGitHub <noreply@github.com>2022-09-16 11:19:16 +1000
commit15110b732a4a24c9bd611477627bdd06291e10b2 (patch)
treebfee8419db8945ab1e0c6099eb4dd68832c8ce50
parentec88b67cf1ef843d09bbaff9e2f7f7e31e40021b (diff)
fix(server): i18n etags missing (#3973)
This fixes missing etags from locales assets.
-rw-r--r--crowdin.yml10
-rw-r--r--internal/middlewares/asset_override.go18
-rw-r--r--internal/server/asset.go97
-rw-r--r--internal/server/const.go17
-rw-r--r--internal/server/handlers.go18
-rw-r--r--internal/server/locales/de-DE/portal.json (renamed from internal/server/locales/de/portal.json)0
-rw-r--r--internal/server/locales/es-ES/portal.json (renamed from internal/server/locales/es/portal.json)0
-rw-r--r--internal/server/locales/fr-FR/portal.json (renamed from internal/server/locales/fr/portal.json)0
-rw-r--r--internal/server/locales/nl-NL/portal.json (renamed from internal/server/locales/nl/portal.json)0
-rw-r--r--internal/server/locales/pt-PT/portal.json (renamed from internal/server/locales/pt/portal.json)0
-rw-r--r--internal/server/locales/ru-RU/portal.json (renamed from internal/server/locales/ru/portal.json)0
-rw-r--r--internal/server/locales/sv/portal.json69
-rw-r--r--internal/server/locales/zh-CN/portal.json (renamed from internal/server/locales/zh/portal.json)0
-rw-r--r--internal/server/template.go17
-rw-r--r--web/src/i18n/index.ts4
15 files changed, 115 insertions, 135 deletions
diff --git a/crowdin.yml b/crowdin.yml
index 51597bf01..88613ec99 100644
--- a/crowdin.yml
+++ b/crowdin.yml
@@ -6,14 +6,4 @@ files:
- source: /internal/server/locales/en/*
translation: /internal/server/locales/%locale%/%original_file_name%
skip_untranslated_files: true
- languages_mapping:
- locale:
- "de-DE": de
- "en-EN": en
- "es-ES": es
- "fr-FR": fr
- "nl-NL": nl
- "pt-PT": pt
- "ru-RU": ru
- "zh-CH": zh
...
diff --git a/internal/middlewares/asset_override.go b/internal/middlewares/asset_override.go
index e25444aba..c8ed111c4 100644
--- a/internal/middlewares/asset_override.go
+++ b/internal/middlewares/asset_override.go
@@ -9,20 +9,22 @@ import (
// AssetOverride allows overriding and serving of specific embedded assets from disk.
func AssetOverride(root string, strip int, next fasthttp.RequestHandler) fasthttp.RequestHandler {
- return func(ctx *fasthttp.RequestCtx) {
- if root == "" {
- next(ctx)
+ if root == "" {
+ return next
+ }
- return
- }
+ handler := fasthttp.FSHandler(root, strip)
+ stripper := fasthttp.NewPathSlashesStripper(strip)
+
+ return func(ctx *fasthttp.RequestCtx) {
+ asset := filepath.Join(root, string(stripper(ctx)))
- _, err := os.Stat(filepath.Join(root, string(fasthttp.NewPathSlashesStripper(strip)(ctx))))
- if err != nil {
+ if _, err := os.Stat(asset); err != nil {
next(ctx)
return
}
- fasthttp.FSHandler(root, strip)(ctx)
+ handler(ctx)
}
}
diff --git a/internal/server/asset.go b/internal/server/asset.go
index 5c4559a9b..57905a283 100644
--- a/internal/server/asset.go
+++ b/internal/server/asset.go
@@ -20,19 +20,21 @@ import (
"github.com/authelia/authelia/v4/internal/utils"
)
-//go:embed locales
-var locales embed.FS
+var (
+ //go:embed public_html
+ assets embed.FS
-//go:embed public_html
-var assets embed.FS
+ //go:embed locales
+ locales embed.FS
+)
func newPublicHTMLEmbeddedHandler() fasthttp.RequestHandler {
etags := map[string][]byte{}
- getEmbedETags(assets, "public_html", etags)
+ getEmbedETags(assets, assetsRoot, etags)
return func(ctx *fasthttp.RequestCtx) {
- p := path.Join("public_html", string(ctx.Path()))
+ p := path.Join(assetsRoot, string(ctx.Path()))
if etag, ok := etags[p]; ok {
ctx.Response.Header.SetBytesKV(headerETag, etag)
@@ -66,8 +68,10 @@ func newPublicHTMLEmbeddedHandler() fasthttp.RequestHandler {
}
}
-func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) {
- var languages []string
+func newLocalesPathResolver() func(ctx *fasthttp.RequestCtx) (supported bool, asset string) {
+ var (
+ languages, dirs []string
+ )
entries, err := locales.ReadDir("locales")
if err == nil {
@@ -84,6 +88,10 @@ func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) {
lng = strings.SplitN(entry.Name(), "-", 2)[0]
}
+ if !utils.IsStringInSlice(entry.Name(), dirs) {
+ dirs = append(dirs, entry.Name())
+ }
+
if utils.IsStringInSlice(lng, languages) {
continue
}
@@ -93,34 +101,79 @@ func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) {
}
}
- return func(ctx *fasthttp.RequestCtx) {
- var (
- language, variant, locale, namespace string
- )
+ aliases := map[string]string{
+ "sv": "sv-SE",
+ "zh": "zh-CN",
+ }
- language = ctx.UserValue("language").(string)
- namespace = ctx.UserValue("namespace").(string)
- locale = language
+ return func(ctx *fasthttp.RequestCtx) (supported bool, asset string) {
+ var language, namespace, variant, locale string
+
+ language, namespace = ctx.UserValue("language").(string), ctx.UserValue("namespace").(string)
+
+ if !utils.IsStringInSlice(language, languages) {
+ return false, ""
+ }
if v := ctx.UserValue("variant"); v != nil {
variant = v.(string)
locale = fmt.Sprintf("%s-%s", language, variant)
+ } else {
+ locale = language
}
- var data []byte
+ ll := language + "-" + strings.ToUpper(language)
+ alias, ok := aliases[locale]
+
+ switch {
+ case ok:
+ return true, fmt.Sprintf("locales/%s/%s.json", alias, namespace)
+ case utils.IsStringInSlice(locale, dirs):
+ return true, fmt.Sprintf("locales/%s/%s.json", locale, namespace)
+ case utils.IsStringInSlice(ll, dirs):
+ return true, fmt.Sprintf("locales/%s-%s/%s.json", language, strings.ToUpper(language), namespace)
+ default:
+ return true, fmt.Sprintf("locales/%s/%s.json", locale, namespace)
+ }
+ }
+}
- if data, err = locales.ReadFile(fmt.Sprintf("locales/%s/%s.json", locale, namespace)); err != nil {
- if utils.IsStringInSliceFold(language, languages) {
- data = []byte("{}")
- }
+func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) {
+ etags := map[string][]byte{}
- if len(data) == 0 {
- hfsHandleErr(ctx, err)
+ getEmbedETags(locales, "locales", etags)
+
+ getAssetName := newLocalesPathResolver()
+
+ return func(ctx *fasthttp.RequestCtx) {
+ supported, asset := getAssetName(ctx)
+
+ if !supported {
+ handlers.SetStatusCodeResponse(ctx, fasthttp.StatusNotFound)
+
+ return
+ }
+
+ if etag, ok := etags[asset]; ok {
+ ctx.Response.Header.SetBytesKV(headerETag, etag)
+ ctx.Response.Header.SetBytesKV(headerCacheControl, headerValueCacheControlETaggedAssets)
+
+ if bytes.Equal(etag, ctx.Request.Header.PeekBytes(headerIfNoneMatch)) {
+ ctx.SetStatusCode(fasthttp.StatusNotModified)
return
}
}
+ var (
+ data []byte
+ err error
+ )
+
+ if data, err = locales.ReadFile(asset); err != nil {
+ data = []byte("{}")
+ }
+
middlewares.SetContentTypeApplicationJSON(ctx)
ctx.SetBody(data)
diff --git a/internal/server/const.go b/internal/server/const.go
index 9c9a34098..4d7a636b2 100644
--- a/internal/server/const.go
+++ b/internal/server/const.go
@@ -5,16 +5,17 @@ import (
)
const (
- embeddedAssets = "public_html/"
- swaggerAssets = embeddedAssets + "api/"
- apiFile = "openapi.yml"
- indexFile = "index.html"
- logoFile = "logo.png"
+ assetsRoot = "public_html"
+ assetsSwagger = assetsRoot + "/api"
+
+ fileOpenAPI = "openapi.yml"
+ fileIndexHTML = "index.html"
+ fileLogo = "logo.png"
)
var (
- rootFiles = []string{"manifest.json", "robots.txt"}
- swaggerFiles = []string{
+ filesRoot = []string{"manifest.json", "robots.txt"}
+ filesSwagger = []string{
"favicon-16x16.png",
"favicon-32x32.png",
"index.css",
@@ -35,7 +36,7 @@ var (
}
// Directories excluded from the not found handler proceeding to the next() handler.
- httpServerDirs = []struct {
+ dirsHTTPServer = []struct {
name, prefix string
}{
{name: "/api", prefix: "/api/"},
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index 389743d39..9f77e0bbb 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -79,8 +79,8 @@ func handleNotFound(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
path := strings.ToLower(string(ctx.Path()))
- for i := 0; i < len(httpServerDirs); i++ {
- if path == httpServerDirs[i].name || strings.HasPrefix(path, httpServerDirs[i].prefix) {
+ for i := 0; i < len(dirsHTTPServer); i++ {
+ if path == dirsHTTPServer[i].name || strings.HasPrefix(path, dirsHTTPServer[i].prefix) {
handlers.SetStatusCodeResponse(ctx, fasthttp.StatusNotFound)
return
@@ -104,9 +104,9 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
https := config.Server.TLS.Key != "" && config.Server.TLS.Certificate != ""
- serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
- serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
- serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
+ serveIndexHandler := ServeTemplatedFile(assetsRoot, fileIndexHTML, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
+ serveSwaggerHandler := ServeTemplatedFile(assetsSwagger, fileIndexHTML, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
+ serveSwaggerAPIHandler := ServeTemplatedFile(assetsSwagger, fileOpenAPI, config.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, config.Session.Name, config.Theme, https)
handlerPublicHTML := newPublicHTMLEmbeddedHandler()
handlerLocales := newLocalesEmbeddedHandler()
@@ -124,7 +124,7 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
// Static Assets.
r.GET("/", middleware(serveIndexHandler))
- for _, f := range rootFiles {
+ for _, f := range filesRoot {
r.GET("/"+f, handlerPublicHTML)
}
@@ -139,10 +139,10 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
// Swagger.
r.GET("/api/", middleware(serveSwaggerHandler))
r.OPTIONS("/api/", policyCORSPublicGET.HandleOPTIONS)
- r.GET("/api/"+apiFile, policyCORSPublicGET.Middleware(middleware(serveSwaggerAPIHandler)))
- r.OPTIONS("/api/"+apiFile, policyCORSPublicGET.HandleOPTIONS)
+ r.GET("/api/"+fileOpenAPI, policyCORSPublicGET.Middleware(middleware(serveSwaggerAPIHandler)))
+ r.OPTIONS("/api/"+fileOpenAPI, policyCORSPublicGET.HandleOPTIONS)
- for _, file := range swaggerFiles {
+ for _, file := range filesSwagger {
r.GET("/api/"+file, handlerPublicHTML)
}
diff --git a/internal/server/locales/de/portal.json b/internal/server/locales/de-DE/portal.json
index bc7995099..bc7995099 100644
--- a/internal/server/locales/de/portal.json
+++ b/internal/server/locales/de-DE/portal.json
diff --git a/internal/server/locales/es/portal.json b/internal/server/locales/es-ES/portal.json
index 782ba166e..782ba166e 100644
--- a/internal/server/locales/es/portal.json
+++ b/internal/server/locales/es-ES/portal.json
diff --git a/internal/server/locales/fr/portal.json b/internal/server/locales/fr-FR/portal.json
index 8df78625f..8df78625f 100644
--- a/internal/server/locales/fr/portal.json
+++ b/internal/server/locales/fr-FR/portal.json
diff --git a/internal/server/locales/nl/portal.json b/internal/server/locales/nl-NL/portal.json
index f31e38c7e..f31e38c7e 100644
--- a/internal/server/locales/nl/portal.json
+++ b/internal/server/locales/nl-NL/portal.json
diff --git a/internal/server/locales/pt/portal.json b/internal/server/locales/pt-PT/portal.json
index 7cc993a44..7cc993a44 100644
--- a/internal/server/locales/pt/portal.json
+++ b/internal/server/locales/pt-PT/portal.json
diff --git a/internal/server/locales/ru/portal.json b/internal/server/locales/ru-RU/portal.json
index e7586c4c6..e7586c4c6 100644
--- a/internal/server/locales/ru/portal.json
+++ b/internal/server/locales/ru-RU/portal.json
diff --git a/internal/server/locales/sv/portal.json b/internal/server/locales/sv/portal.json
deleted file mode 100644
index 20f8e1948..000000000
--- a/internal/server/locales/sv/portal.json
+++ /dev/null
@@ -1,69 +0,0 @@
-{
- "Accept": "Acceptera",
- "Access your email addresses": "Hantera din e-postadress",
- "Access your group membership": "Hantera dina gruppmedlemskap",
- "Access your profile information": "Hantera din profilinformation",
- "An email has been sent to your address to complete the process": "Ett mejl har skickats till din e-postadress för att slutföra processen.",
- "Authenticated": "Autentiserad",
- "Cancel": "Avbryt",
- "Client ID": "Klient-ID: {{client_id}}",
- "Consent Request": "Begäran om medgivande",
- "Contact your administrator to register a device": "Kontakta din administratör för att registrera en enhet.",
- "Could not obtain user settings": "Misslyckades med att hämta användarinställningarna.",
- "Deny": "Avböj",
- "Done": "Klar",
- "Enter new password": "Skriv ditt nya lösenord",
- "Enter one-time password": "Skriv ditt engångslösenord",
- "Failed to register device, the provided link is expired or has already been used": "Enhetsregistreringen misslyckades, den angivna länken har utgått eller redan blivit använd.",
- "Hi": "Hej",
- "Incorrect username or password": "Fel användarnamn eller lösenord",
- "Loading": "Läser in",
- "Logout": "Logga ut",
- "Lost your device?": "Har du tappat bort din enhet?",
- "Methods": "Metoder",
- "Must be at least {{len}} characters in length": "Måste vara minst {{len}} tecken långt",
- "Must have at least one UPPERCASE letter": "Måste innehålla minst en stor bokstav",
- "Must have at least one lowercase letter": "Måste innehålla minst en liten bokstav",
- "Must have at least one number": "Måste innehålla minst ett nummer",
- "Must have at least one special character": "Måste innehålla minst ett specialtecken",
- "Must not be more than {{len}} characters in length": "Får inte vara längre än {{len}} tecken",
- "Need Google Authenticator?": "Behöver du Google Authenticator?",
- "New password": "Nytt lösenord",
- "No verification token provided": "Ingen verifieringskod tillhandahålls",
- "OTP Secret copied to clipboard": "OTP koden har kopierats till urklipp",
- "OTP URL copied to clipboard": "OTP länken har kopierats till urklipp",
- "One-Time Password": "Engångslösenord",
- "Password has been reset": "Lösenordet har blivit återställt",
- "Password": "Lösenord",
- "Passwords do not match": "Lösenorden matchar inte",
- "Push Notification": "Push-avisering",
- "Register device": "Registrera enhet",
- "Register your first device by clicking on the link below": "Registrera din första enhet genom att klicka på länken nedan",
- "Remember Consent": "Kom ihåg samtycke",
- "Remember me": "Kom ihåg mig",
- "Repeat new password": "Upprepa nya lösenordet",
- "Reset password": "Återställ lösenord",
- "Reset password?": "Återställ lösenord?",
- "Reset": "Återställ",
- "Scan QR Code": "Skanna QR koden",
- "Secret": "Kod",
- "Security Key - WebAuthN": "Säkerhetsnyckel - WebAuthN",
- "Select a Device": "Välj en enhet",
- "Sign in": "Logga in",
- "Sign out": "Logga ut",
- "The above application is requesting the following permissions": "Ovanstående program begär följande behörigheter",
- "The password does not meet the password policy": "Lösenordet uppfyller inte lösenordspolicyn",
- "The resource you're attempting to access requires two-factor authentication": "Resursen du vill komma åt kräver tvåstegsverifiering",
- "There was a problem initiating the registration process": "Ett problem uppstod när registreringsprocessen skulle starta",
- "There was an issue completing the process. The verification token might have expired": "Det uppstod ett problem med att slutföra processen. Verifieringskoden kan ha gått ut",
- "There was an issue initiating the password reset process": "Ett problem uppstod när processen för lösenordsåterställning startade",
- "There was an issue resetting the password": "Ett problem uppstod med att återställa lösenordet",
- "There was an issue signing out": "Ett problem uppstod med att logga ut",
- "This saves this consent as a pre-configured consent for future use": "Spara detta samtycke som ett förkonfigurerat samtycke för framtida användning",
- "Time-based One-Time Password": "Tidsbaserat engångslösenord",
- "Use OpenID to verify your identity": "Använd OpenID till att verifiera din identitet",
- "Username": "Användarnamn",
- "You must open the link from the same device and browser that initiated the registration process": "Du måste öppna länken från samma enhet och webbläsare som startade registreringsprocessen",
- "You're being signed out and redirected": "Du blir utloggad och omdirigerad",
- "Your supplied password does not meet the password policy requirements": "Det angivna lösenordet möter inte lösenordskraven"
-}
diff --git a/internal/server/locales/zh/portal.json b/internal/server/locales/zh-CN/portal.json
index 1f570dddf..1f570dddf 100644
--- a/internal/server/locales/zh/portal.json
+++ b/internal/server/locales/zh-CN/portal.json
diff --git a/internal/server/template.go b/internal/server/template.go
index 9c5bb523d..fedf2df57 100644
--- a/internal/server/template.go
+++ b/internal/server/template.go
@@ -4,10 +4,13 @@ import (
"fmt"
"io"
"os"
+ "path"
"path/filepath"
"strings"
"text/template"
+ "github.com/valyala/fasthttp"
+
"github.com/authelia/authelia/v4/internal/logging"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/utils"
@@ -19,7 +22,7 @@ import (
func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, session, theme string, https bool) middlewares.RequestHandler {
logger := logging.Logger()
- a, err := assets.Open(publicDir + file)
+ a, err := assets.Open(path.Join(publicDir, file))
if err != nil {
logger.Fatalf("Unable to open %s: %s", file, err)
}
@@ -43,7 +46,7 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM
logoOverride := f
if assetPath != "" {
- if _, err := os.Stat(filepath.Join(assetPath, logoFile)); err == nil {
+ if _, err := os.Stat(filepath.Join(assetPath, fileLogo)); err == nil {
logoOverride = t
}
}
@@ -71,14 +74,14 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM
}
switch {
- case publicDir == swaggerAssets:
- ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("base-uri 'self'; default-src 'self'; img-src 'self' https://validator.swagger.io data:; object-src 'none'; script-src 'self' 'unsafe-inline' 'nonce-%s'; style-src 'self' 'nonce-%s'", nonce, nonce))
+ case publicDir == assetsSwagger:
+ ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf("base-uri 'self'; default-src 'self'; img-src 'self' https://validator.swagger.io data:; object-src 'none'; script-src 'self' 'unsafe-inline' 'nonce-%s'; style-src 'self' 'nonce-%s'", nonce, nonce))
case ctx.Configuration.Server.Headers.CSPTemplate != "":
- ctx.Response.Header.Add("Content-Security-Policy", strings.ReplaceAll(ctx.Configuration.Server.Headers.CSPTemplate, cspNoncePlaceholder, nonce))
+ ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, strings.ReplaceAll(ctx.Configuration.Server.Headers.CSPTemplate, cspNoncePlaceholder, nonce))
case os.Getenv("ENVIRONMENT") == dev:
- ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf(cspDefaultTemplate, " 'unsafe-eval'", nonce))
+ ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(cspDefaultTemplate, " 'unsafe-eval'", nonce))
default:
- ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf(cspDefaultTemplate, "", nonce))
+ ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(cspDefaultTemplate, "", nonce))
}
err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, BaseURL, CSPNonce, DuoSelfEnrollment, LogoOverride, RememberMe, ResetPassword, ResetPasswordCustomURL, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, DuoSelfEnrollment: duoSelfEnrollment, LogoOverride: logoOverride, RememberMe: rememberMe, ResetPassword: resetPassword, ResetPasswordCustomURL: resetPasswordCustomURL, Session: session, Theme: theme})
diff --git a/web/src/i18n/index.ts b/web/src/i18n/index.ts
index 53231ccd1..6cfa4419d 100644
--- a/web/src/i18n/index.ts
+++ b/web/src/i18n/index.ts
@@ -18,9 +18,9 @@ i18n.use(Backend)
backend: {
loadPath: basePath + "/locales/{{lng}}/{{ns}}.json",
},
+ load: "all",
ns: ["portal"],
defaultNS: "portal",
- load: "all",
fallbackLng: {
default: ["en"],
de: ["en"],
@@ -33,7 +33,7 @@ i18n.use(Backend)
"sv-SE": ["sv", "en"],
zh: ["en"],
"zh-CN": ["zh", "en"],
- "zh-TW": ["zh", "en"],
+ "zh-TW": ["en"],
},
supportedLngs: ["en", "de", "es", "fr", "nl", "pt", "ru", "sv", "sv-SE", "zh", "zh-CN", "zh-TW"],
lowerCaseLng: false,