diff options
| author | Felix Hanley <felix@userspace.com.au> | 2025-08-28 01:38:06 +0000 |
|---|---|---|
| committer | Felix Hanley <felix@userspace.com.au> | 2025-08-28 01:38:06 +0000 |
| commit | 02e6f97cd04cbcd7505e1da9a781df4321463640 (patch) | |
| tree | 08d3d2317cdab4885d7c9830ed7983fecfb9fb4a /vendor/github.com/invopop/ctxi18n | |
| parent | faa33e32b5e967fdfeac96bfc39ed3d94f9514ac (diff) | |
| download | caddy-02e6f97cd04cbcd7505e1da9a781df4321463640.tar.gz caddy-02e6f97cd04cbcd7505e1da9a781df4321463640.tar.bz2 | |
Attempt to stop AI bots using Anubis
Diffstat (limited to 'vendor/github.com/invopop/ctxi18n')
| -rw-r--r-- | vendor/github.com/invopop/ctxi18n/.gitignore | 21 | ||||
| -rw-r--r-- | vendor/github.com/invopop/ctxi18n/.golangci.yaml | 30 | ||||
| -rw-r--r-- | vendor/github.com/invopop/ctxi18n/LICENSE | 201 | ||||
| -rw-r--r-- | vendor/github.com/invopop/ctxi18n/README.md | 256 | ||||
| -rw-r--r-- | vendor/github.com/invopop/ctxi18n/ctxi18n.go | 74 | ||||
| -rw-r--r-- | vendor/github.com/invopop/ctxi18n/i18n/code.go | 41 | ||||
| -rw-r--r-- | vendor/github.com/invopop/ctxi18n/i18n/dict.go | 103 | ||||
| -rw-r--r-- | vendor/github.com/invopop/ctxi18n/i18n/i18n.go | 86 | ||||
| -rw-r--r-- | vendor/github.com/invopop/ctxi18n/i18n/locale.go | 129 | ||||
| -rw-r--r-- | vendor/github.com/invopop/ctxi18n/i18n/locales.go | 120 | ||||
| -rw-r--r-- | vendor/github.com/invopop/ctxi18n/i18n/plural_rules.go | 42 |
11 files changed, 1103 insertions, 0 deletions
diff --git a/vendor/github.com/invopop/ctxi18n/.gitignore b/vendor/github.com/invopop/ctxi18n/.gitignore new file mode 100644 index 0000000..3b735ec --- /dev/null +++ b/vendor/github.com/invopop/ctxi18n/.gitignore @@ -0,0 +1,21 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work diff --git a/vendor/github.com/invopop/ctxi18n/.golangci.yaml b/vendor/github.com/invopop/ctxi18n/.golangci.yaml new file mode 100644 index 0000000..1925251 --- /dev/null +++ b/vendor/github.com/invopop/ctxi18n/.golangci.yaml @@ -0,0 +1,30 @@ +run: + timeout: "120s" + +output: + formats: + - format: "colored-line-number" + +linters: + enable: + - "gocyclo" + - "unconvert" + - "goimports" + - "govet" + #- "misspell" # doesn't handle multilanguage well + - "nakedret" + - "revive" + - "goconst" + - "unparam" + - "gofmt" + - "errname" + - "zerologlint" + +linters-settings: + staticcheck: + # SAxxxx checks in https://staticcheck.io/docs/configuration/options/#checks + # Default: ["*"] + checks: ["all"] + +issues: + exclude-use-default: false diff --git a/vendor/github.com/invopop/ctxi18n/LICENSE b/vendor/github.com/invopop/ctxi18n/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/vendor/github.com/invopop/ctxi18n/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/invopop/ctxi18n/README.md b/vendor/github.com/invopop/ctxi18n/README.md new file mode 100644 index 0000000..61117b7 --- /dev/null +++ b/vendor/github.com/invopop/ctxi18n/README.md @@ -0,0 +1,256 @@ +# ctxi18n + +[](https://github.com/invopop/ctxi18n/actions/workflows/lint.yaml) +[](https://github.com/invopop/ctxi18n/actions/workflows/test.yaml) +[](https://goreportcard.com/report/github.com/invopop/ctxi18n) +[](https://codecov.io/gh/invopop/ctxi18n) +[](https://godoc.org/github.com/invopop/ctxi18n) + + +Go Context Internationalization - translating apps easily. + +## Introduction + +`ctxi18n` is heavily influenced by [internationalization in Ruby on Rails](https://guides.rubyonrails.org/i18n.html) and aims to make it just as straightforward in Go applications. + +As the name suggests, `ctxi18n` focusses on making i18n data available inside an application's context instances, but is sufficiently flexible to used directly if needed. + +Key Features: + +- Loads locale files written in YAML or JSON with a similar structure those in Ruby i18n. +- Makes it easy to add a locale object to the context. +- Supports `fs.FS` to load data. +- Short method names like `i18n.T()` or `i18n.N()`. +- Support for simple interpolation using keys, e.g. `Some %{key} text` +- Support for pluralization rules. +- Default values when translations are missing. + +## Usage + +Import the library with: + +```go +import "github.com/invopop/ctxi18n" +``` + +First you'll need to load YAML or JSON translation definitions. Files may be named and structured however you like, but the contents must always follow the same pattern of language and properties, for example: + +```yaml +en: + welcome: + title: "Welcome to our application!" + login: "Log in" + signup: "Sign up" + forgot-password: "Forgot Password?" +es: + welcome: + title: "¡Bienvenido a nuestra aplicación!" + login: "Ingresarse" + signup: "Registrarse" + forgot-password: "¿Olvidaste tu contraseña? +``` + +The first level of properties of the object **must** always define the locale that the rest of sub-object's contents will provide translations for. + +Files will all be deep-merged on top of each other so you can safely extend dictionaries from multiple sources. + +To load the dictionary run something like the following where the `asset.Content` is a package containing [embedded files](https://pkg.go.dev/embed): + +```go +if err := ctxi18n.Load(assets.Content); err != nil { + panic(err) +} +``` + +If you'd like to set a default base language to try to use for any missing translations, load the assets with a default: + +```go +if err := ctxi18n.LoadWithDefault(assets.Content, "en"); err != nil { + panic(err) +} +``` + +You'll now have a global set of locales prepared in memory and ready to use. Assuming your application uses some kind of context such as from an HTTP or gRPC request, you'll want to add a single locale to it: + +```go +ctx = ctxi18n.WithLocale(ctx, "en") +``` + +Locale selection is performed according to [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html) and the `Accept-Language` header, so you can pass in a code string and an attempt will be made to find the best match: + +```go +ctx = ctxi18n.WithLocale(ctx, "en-US,en;q=0.9,es;q=0.8") +``` + +In this example, the first locale to matched will be `en-US`, followed by just `en`, then `es`: + +Getting translations is straightforward, you have two options: + +1. call methods defined in the package with the context, or, +2. extract the locale from the context and use. + +To translate without extracting the locale, you'll need to load the `i18n` package which contains all the structures and methods used by the main `ctxi18n` without any globals: + +```go +import "github.com/invopop/ctxi18n/i18n" +``` + +Then use it with the context: + +```go +fmt.Println(i18n.T(ctx, "welcome.title")) +``` + +Notice in the example that `title` was previously defined inside the `welcome` object in the source YAML, and we're accessing it here by defining the path `welcome.title`. + +To use the `Locale` object directly, extract it from the context and call the methods: + +```go +l := ctxi18n.Locale(ctx) +fmt.Println(l.T("welcome.title")) +``` + +There is no preferred way on how to use this library, so please use whatever best first your application and coding style. Sometimes it makes sense to pass in the context in every call, other times the code can be shorter and more concise by extracting it. + +### Defaults + +If a translation is missing from the locale a "missing" text will be produced, for example: + +```go +fmt.Println(l.T("welcome.no.text")) +``` + +Will return a text that follows the `fmt.Sprintf` missing convention: + +``` +!(MISSING welcome.no.text) +``` + +This can be useful for translators to figure out which texts are missing, but sometimes a default value is more appropriate: + +```go +fmt.Println(i18n.T(ctx, "welcome.question", i18n.Default("Just ask!"))) +// output: "Just ask!" +code := "EUR" +fmt.Println(i18n.T(ctx, "currencies."+code, i18n.Default(code))) +// output: "EUR" +``` + +An alternative to using defaults is to check if the key exists using the `Has` method: + +```go +if !i18n.Has(ctx, "welcome.question") { + fmt.Println("Just ask!") +} +``` + +### Interpolation + +Go's default approach for interpolation using the `fmt.Sprintf` and related methods is good for simple use-cases. For example, given the following translation: + +```yaml +en: + welcome: + title: "Hi %s, welcome to our App!" +``` + +You can get the translated text and interpolate with: + +```go +i18n.T(ctx, "welcome.title", "Sam") +``` + +This however is an _anti-pattern_ when it comes to translating applications as translators may need to change the order of replaced words. To get around this, `ctxi18n` supports simple named interpolation as follows: + +```yaml +en: + welcome: + title: "Hi %{name}, welcome to our App!" +``` + +```go +i18n.T(ctx, "welcome.title", i18n.M{"name":"Sam"}) +``` + +The `i18n.M` map is used to perform a simple find and replace on the matching translation. The `fmt.Sprint` method is used to convert values into strings, so you don't need to worry about simple serialization like for numbers. + +Interpolation can also be used alongside default values: + +```go +i18n.T(ctx, "welcome.title", i18n.Default("Hi %{name}"), i18n.M{"name":"Sam"}) +``` + +## Pluralization + +When texts include references to numbers we need internationalization libraries like `ctxi18n` that help define multiple possible translations according to a number. Pluralized translations are defined like this: + +```yaml +en: + inbox: + emails: + zero: "You have no emails." + one: "You have %{count} email." + other: "You have %{count} emails. +``` + +The `inbox.emails` tag has a sub-object that defines all the translations we need according to the pluralization rules of the language. In the case of English which uses the default rule set, `zero` is an optional definition that will be used if provided and fallback on `other` if not. + +To use these translations, call the `i18n.N` method: + +```go +count := 2 +fmt.Println(i18n.N(ctx, "inbox.emails", count, i18n.M{"count": count})) +``` + +The output from this will be: "You have 2 emails." + +In the current implementation of `ctxi18n` there are very few pluralization rules defined, please submit PRs if your language is not covered! + +## Scopes + +As your application gets more complex, it can get repetitive having to use the same base keys. To get around this, use the `WithScope` helper method inside a context: + +```go +ctx := i18n.WithScope(ctx, "welcome") +i18n.T(ctx, ".title", i18n.M{"name":"Sam"}) +``` + +Anything with the `.` at the beginning will append the scope. You can continue to use any other key in the locale by not using the `.` at the front. + +## Templ + +[Templ](https://templ.guide/) is a templating library that helps you create components that render fragments of HTML and compose them to create screens, pages, documents or apps. + +The following "Hello World" example is taken from the [Templ Guide](https://templ.guide) and shows how you can quickly add translations the leverage the built-in `ctx` variable provided by Templ. + +```yaml +en: + welcome: + hello: "Hello, %{name}" +``` + +```go +package main + +import "github.com/invopop/ctxi18n/i18n" + +templ Hello(name string) { + <span class="hello"> + { i18n.T(ctx, "welcome.hello", i18n.M{"name": name}) } + </span> +} + +templ Greeting(person Person) { + <div class="greeting"> + @Hello(person.Name) + </div> +} +``` + +To save even more typing, it might be worth defining your own templ wrappers around those defined in the `i18n` package. Check out the [gobl.html `t` package](https://github.com/invopop/gobl.html/tree/main/components/t) for an example. + +# Examples + +The following is a list of Open Source projects using this library from which you can see working examples for your own solutions. Please send in a PR if you'd like to add your project! + +- [GOBL HTML](https://github.com/invopop/gobl.html) - generate HTML files like invoices from [GOBL](https://gobl.org) documents. diff --git a/vendor/github.com/invopop/ctxi18n/ctxi18n.go b/vendor/github.com/invopop/ctxi18n/ctxi18n.go new file mode 100644 index 0000000..26b6080 --- /dev/null +++ b/vendor/github.com/invopop/ctxi18n/ctxi18n.go @@ -0,0 +1,74 @@ +// Package ctxi18n is used to internationalize applications using the context +// for the locale. +package ctxi18n + +import ( + "context" + "errors" + "io/fs" + + "github.com/invopop/ctxi18n/i18n" +) + +var ( + // DefaultLocale defines the default or fallback locale code to use + // if no other match inside the packages list was found. + DefaultLocale i18n.Code = "en" +) + +var ( + locales *i18n.Locales +) + +var ( + // ErrMissingLocale implies that the requested locale was not found + // in the current index. + ErrMissingLocale = errors.New("locale not defined") +) + +func init() { + locales = new(i18n.Locales) +} + +// Load walks through all the files in provided File System and prepares +// an internal global list of locales ready to use. +func Load(fs fs.FS) error { + return locales.Load(fs) +} + +// LoadWithDefault performs the regular load operation, but will merge +// the default locale with every other locale, ensuring that every text +// has at least the value from the default locale. +func LoadWithDefault(fs fs.FS, locale i18n.Code) error { + return locales.LoadWithDefault(fs, locale) +} + +// Get provides the Locale object for the matching code. +func Get(code i18n.Code) *i18n.Locale { + return locales.Get(code) +} + +// Match attempts to find the best possible matching locale based on the +// locale string provided. The locale string is parsed according to the +// "Accept-Language" header format defined in RFC9110. +func Match(locale string) *i18n.Locale { + return locales.Match(locale) +} + +// WithLocale tries to match the provided code with a locale and ensures +// it is available inside the context. +func WithLocale(ctx context.Context, locale string) (context.Context, error) { + l := locales.Match(locale) + if l == nil { + l = locales.Get(DefaultLocale) + if l == nil { + return nil, ErrMissingLocale + } + } + return l.WithContext(ctx), nil +} + +// Locale provides the locale object currently stored in the context. +func Locale(ctx context.Context) *i18n.Locale { + return i18n.GetLocale(ctx) +} diff --git a/vendor/github.com/invopop/ctxi18n/i18n/code.go b/vendor/github.com/invopop/ctxi18n/i18n/code.go new file mode 100644 index 0000000..f9f408e --- /dev/null +++ b/vendor/github.com/invopop/ctxi18n/i18n/code.go @@ -0,0 +1,41 @@ +package i18n + +import "strings" + +// Code is used to represent a language code which follows the +// ISO 639-1 standard, with sub-tags aggregated with hyphens, +// as defined in [RFC5646](https://datatracker.ietf.org/doc/html/rfc5646). +// Examples include: +// +// fr, en-US, es-419, az-Arab, x-pig-latin, man-Nkoo-GN +type Code string + +// String returns the string variant of the code. +func (c Code) String() string { + return string(c) +} + +// Base returns the base language code, without any subtags. +func (c Code) Base() Code { + out := strings.SplitN(c.String(), "-", 2) + return Code(out[0]) +} + +// ParseAcceptLanguage provides an ordered set of codes extracted +// from an HTTP "Accept-Language" header as defined in RFC9110. +// Current implementation will ignore quality values and instead +// just assume the order of the provided codes is valid. +func ParseAcceptLanguage(txt string) []Code { + list := make([]Code, 0) + for _, s := range strings.Split(txt, ",") { + s = strings.TrimSpace(s) + + // Remove any quality values. + if i := strings.Index(s, ";"); i > 0 { + s = s[:i] + } + + list = append(list, Code(s)) + } + return list +} diff --git a/vendor/github.com/invopop/ctxi18n/i18n/dict.go b/vendor/github.com/invopop/ctxi18n/i18n/dict.go new file mode 100644 index 0000000..6ff32ba --- /dev/null +++ b/vendor/github.com/invopop/ctxi18n/i18n/dict.go @@ -0,0 +1,103 @@ +package i18n + +import ( + "encoding/json" + "strings" +) + +// Dict holds the internationalization entries for a specific locale. +type Dict struct { + value string + entries map[string]*Dict +} + +// NewDict instantiates a new dict object. +func NewDict() *Dict { + return &Dict{ + entries: make(map[string]*Dict), + } +} + +// Add adds a new key value pair to the dictionary. +func (d *Dict) Add(key string, value any) { + switch v := value.(type) { + case string: + d.entries[key] = &Dict{value: v} + case map[string]any: + nd := NewDict() + for k, row := range v { + nd.Add(k, row) + } + d.entries[key] = nd + case *Dict: + d.entries[key] = v + default: + // ignore + } +} + +// Value returns the dictionary value or an empty string +// if the dictionary is nil. +func (d *Dict) Value() string { + if d == nil { + return "" + } + return d.value +} + +// Get recursively retrieves the dictionary at the provided key location. +func (d *Dict) Get(key string) *Dict { + if d == nil { + return nil + } + if key == "" { + return nil + } + n := strings.SplitN(key, ".", 2) + entry, ok := d.entries[n[0]] + if !ok { + return nil + } + if len(n) == 1 { + return entry + } + return entry.Get(n[1]) +} + +// Has is a convenience method to check if a key exists in the dictionary +// recursively, and is the equivalent of calling `Get` and checking if +// the result is not nil. +func (d *Dict) Has(key string) bool { + return d.Get(key) != nil +} + +// Merge combines the entries of the second dictionary into this one. If a +// key is duplicated in the second diction, the original value takes priority. +func (d *Dict) Merge(d2 *Dict) { + if d2 == nil { + return + } + if d.entries == nil { + d.entries = make(map[string]*Dict) + } + for k, v := range d2.entries { + if d.entries[k] == nil { + d.entries[k] = v + continue + } + d.entries[k].Merge(v) + } +} + +// UnmarshalJSON attempts to load the dictionary data from a JSON byte slice. +func (d *Dict) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + return nil + } + if data[0] == '"' { + d.value = string(data[1 : len(data)-1]) + return nil + } + d.entries = make(map[string]*Dict) + return json.Unmarshal(data, &d.entries) +} diff --git a/vendor/github.com/invopop/ctxi18n/i18n/i18n.go b/vendor/github.com/invopop/ctxi18n/i18n/i18n.go new file mode 100644 index 0000000..d056dce --- /dev/null +++ b/vendor/github.com/invopop/ctxi18n/i18n/i18n.go @@ -0,0 +1,86 @@ +// Package i18n is responsible for keeping the key internationalization in one +// place. +package i18n + +import ( + "context" + "fmt" + "strings" +) + +const ( + missingLocaleOut = "!(MISSING LOCALE)" +) + +type scopeType string + +const ( + scopeKey scopeType = "scope" +) + +// M stands for map and is a simple helper to make it easier to work with +// internationalization maps. +type M map[string]any + +// T is responsible for translating a key into a string by extracting +// the local from the context. +func T(ctx context.Context, key string, args ...any) string { + l := GetLocale(ctx) + if l == nil { + return missingLocaleOut + } + key = ExpandKey(ctx, key) + return l.T(key, args...) +} + +// N returns the pluralized translation of the provided key using n +// as the count. +func N(ctx context.Context, key string, n int, args ...any) string { + l := GetLocale(ctx) + if l == nil { + return missingLocaleOut + } + key = ExpandKey(ctx, key) + return l.N(key, n, args...) +} + +// Has performs a check to see if the key exists in the locale. +func Has(ctx context.Context, key string) bool { + l := GetLocale(ctx) + if l == nil { + return false + } + key = ExpandKey(ctx, key) + return l.Has(key) +} + +// WithScope is used to add a new scope to the context. To use this, +// use a `.` at the beginning of keys. +func WithScope(ctx context.Context, key string) context.Context { + key = ExpandKey(ctx, key) + return context.WithValue(ctx, scopeKey, key) +} + +// ExpandKey extracts the current scope from the context and appends it +// to the start of the provided key. +func ExpandKey(ctx context.Context, key string) string { + if !strings.HasPrefix(key, ".") { + return key + } + scope, ok := ctx.Value(scopeKey).(string) + if !ok { + return key + } + return fmt.Sprintf("%s%s", scope, key) +} + +// Replace is used to interpolate the matched keys in the provided +// string with their values in the map. +// +// Interpolation is performed using the `%{key}` pattern. +func (m M) Replace(in string) string { + for k, v := range m { + in = strings.Replace(in, fmt.Sprintf("%%{%s}", k), fmt.Sprint(v), -1) + } + return in +} diff --git a/vendor/github.com/invopop/ctxi18n/i18n/locale.go b/vendor/github.com/invopop/ctxi18n/i18n/locale.go new file mode 100644 index 0000000..65dbd30 --- /dev/null +++ b/vendor/github.com/invopop/ctxi18n/i18n/locale.go @@ -0,0 +1,129 @@ +package i18n + +import ( + "context" + "encoding/json" + "fmt" +) + +// Locale holds the internationalization entries for a specific locale. +type Locale struct { + code Code + dict *Dict + rule PluralRule +} + +const ( + missingDictOut = "!(MISSING: %s)" + localeKey Code = "locale" +) + +// DefaultText when detected as an argument to a translation +// function will be used if no language match is found. +type DefaultText string + +// Default when used as an argument to a translation function +// ensure the provided txt is used as a default value if no +// language match is found. +func Default(txt string) DefaultText { + return DefaultText(txt) +} + +// NewLocale creates a new locale with the provided key and dictionary. +func NewLocale(code Code, dict *Dict) *Locale { + l := &Locale{ + code: code, + dict: dict, + } + l.rule = mapPluralRule(code) + return l + +} + +// Code returns the language code of the locale. +func (l *Locale) Code() Code { + return l.code +} + +// T provides the value from the dictionary stored by the locale. +func (l *Locale) T(key string, args ...any) string { + return interpolate(key, l.dict.Get(key), args...) +} + +// N uses the locale pluralization rules to determine which +// string value to provide based on the provided number. +func (l *Locale) N(key string, n int, args ...any) string { + d := l.dict.Get(key) + return interpolate(key, l.rule(d, n), args...) +} + +// Has performs a check to see if the key exists in the locale. +// This is useful for checking if a key exists before attempting +// to use it when the Default function cannot be used. +func (l *Locale) Has(key string) bool { + return l.dict.Has(key) +} + +// PluralRule provides the pluralization rule for the locale. +func (l *Locale) PluralRule() PluralRule { + return l.rule +} + +// UnmarshalJSON attempts to load the locale from a JSON byte slice. +func (l *Locale) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + return nil + } + l.dict = new(Dict) + if err := json.Unmarshal(data, l.dict); err != nil { + return err + } + return nil +} + +// WithContext inserts the locale into the context so that it can be +// loaded later with `GetLocale`. +func (l *Locale) WithContext(ctx context.Context) context.Context { + return context.WithValue(ctx, localeKey, l) +} + +// GetLocale retrieves the locale from the context. +func GetLocale(ctx context.Context) *Locale { + if l, ok := ctx.Value(localeKey).(*Locale); ok { + return l + } + return nil +} + +func interpolate(key string, d *Dict, args ...any) string { + var s string + s, args = extractDefault(args) + if d != nil { + s = d.value + } + if s == "" { + return missing(key) + } + if len(args) > 0 { + switch arg := args[0].(type) { + case M: + return arg.Replace(s) + default: + return fmt.Sprintf(s, args...) + } + } + return s +} + +func extractDefault(args []any) (string, []any) { + for i, arg := range args { + if dt, ok := arg.(DefaultText); ok { + return string(dt), append(args[:i], args[i+1:]...) + } + } + return "", args +} + +func missing(key string) string { + return fmt.Sprintf(missingDictOut, key) +} diff --git a/vendor/github.com/invopop/ctxi18n/i18n/locales.go b/vendor/github.com/invopop/ctxi18n/i18n/locales.go new file mode 100644 index 0000000..43d5dcf --- /dev/null +++ b/vendor/github.com/invopop/ctxi18n/i18n/locales.go @@ -0,0 +1,120 @@ +package i18n + +import ( + "encoding/json" + "fmt" + "io/fs" + "path/filepath" + + "github.com/invopop/yaml" +) + +// Locales is a map of language keys to their respective locale. +type Locales struct { + list []*Locale +} + +// Load walks through all the files in the provided File System +// and merges every one with the current list of locales. +func (ls *Locales) Load(src fs.FS) error { + return fs.WalkDir(src, ".", func(path string, _ fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("walking directory: %w", err) + } + + switch filepath.Ext(path) { + case ".yaml", ".yml", ".json": + // good + default: + return nil + } + + data, err := fs.ReadFile(src, path) + if err != nil { + return fmt.Errorf("reading file '%s': %w", path, err) + } + + if err := yaml.Unmarshal(data, ls); err != nil { + return fmt.Errorf("unmarshalling file '%s': %w", path, err) + } + + return nil + }) +} + +// LoadWithDefault performs the regular load operation, but follows up with +// a second operation that will ensure that default dictionary is merged with +// every other locale, thus ensuring that every text will have a fallback. +func (ls *Locales) LoadWithDefault(src fs.FS, locale Code) error { + if err := ls.Load(src); err != nil { + return err + } + + l := ls.Get(locale) + if l == nil { + return fmt.Errorf("undefined default locale: %s", locale) + } + for _, loc := range ls.list { + if loc == l { + continue + } + loc.dict.Merge(l.dict) + } + + return nil +} + +// Get provides the define Locale object for the matching key. +func (ls *Locales) Get(code Code) *Locale { + for _, loc := range ls.list { + if loc.Code() == code { + return loc + } + } + return nil +} + +// Match attempts to find the best possible matching locale based on the +// locale string provided. The locale string is parsed according to the +// "Accept-Language" header format defined in RFC9110. +func (ls *Locales) Match(locale string) *Locale { + codes := ParseAcceptLanguage(locale) + for _, code := range codes { + for _, loc := range ls.list { + if loc.Code() == code { + return loc + } + } + } + return nil +} + +// Codes provides a list of locale codes defined in the +// list. +func (ls *Locales) Codes() []Code { + codes := make([]Code, len(ls.list)) + for i, l := range ls.list { + codes[i] = l.Code() + } + return codes +} + +// UnmarshalJSON attempts to load the locales from a JSON byte slice +// and merge them into any existing locales. +func (ls *Locales) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + return nil + } + aux := make(map[Code]*Dict) + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + for c, v := range aux { + if l := ls.Get(c); l != nil { + l.dict.Merge(v) + } else { + ls.list = append(ls.list, NewLocale(c, v)) + } + } + return nil +} diff --git a/vendor/github.com/invopop/ctxi18n/i18n/plural_rules.go b/vendor/github.com/invopop/ctxi18n/i18n/plural_rules.go new file mode 100644 index 0000000..108a31a --- /dev/null +++ b/vendor/github.com/invopop/ctxi18n/i18n/plural_rules.go @@ -0,0 +1,42 @@ +package i18n + +// Standard pluralization rule keys. +const ( + DefaultRuleKey = "default" +) + +// PluralRule defines a simple method that expects a dictionary and number and +// will find a matching dictionary entry. +type PluralRule func(d *Dict, num int) *Dict + +const ( + zeroKey = "zero" + oneKey = "one" + otherKey = "other" +) + +var rules = map[string]PluralRule{ + // Most languages can use this rule + DefaultRuleKey: func(d *Dict, n int) *Dict { + if n == 0 { + v := d.Get(zeroKey) + if v != nil { + return v + } + } + if n == 1 { + return d.Get(oneKey) + } + return d.Get(otherKey) + }, +} + +// GetRule provides the PluralRule for the given key. +func GetRule(key string) PluralRule { + return rules[key] +} + +// mapPluralRule is used to map a language code into a pluralization rule. +func mapPluralRule(_ Code) PluralRule { + return rules[DefaultRuleKey] +} |
