diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index b78abbd..acd6ef8 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -14,7 +14,7 @@ jobs:
steps:
- uses: actions/setup-go@v4
with:
- go-version: "1.19"
+ go-version: "1.21"
cache: false
- name: Check out code
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 7038274..3ba7a95 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -9,7 +9,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
- go-version: "1.19"
+ go-version: "1.21"
id: go
- name: Check out code
diff --git a/README.md b/README.md
index f152d48..91c1cdd 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,6 @@ Key Features:
- Supports Go's embed FS to load data from inside binary.
- Short method names and usage to Gettext like `i18n.T()` or `i18n.N()`.
- Support for simple interpolation using keys, e.g. `Some %{key} text`
-- Built in support for [templ templating](https://templ.guide/) which uses context throughout.
## Usage
@@ -73,19 +72,19 @@ 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` helper package:
+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"
```
-This package contains helper methods that allow you to use the context directly:
+Then use it with the context:
```go
fmt.Println(i18n.T(ctx, "welcome.title"))
```
-Notice in the example that the `title` was previously defined inside the `welcome` object in the source YAML, and we're accessing it here by defining the path `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:
@@ -139,7 +138,7 @@ en:
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 and the default rule set, `zero` is an optional definition that will be used if provided.
+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:
@@ -150,4 +149,32 @@ 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 do submit PRs if you language is not covered!
+In the current implementation of `ctxi18n` there are very few pluralization rules defined, please submit PRs if your language is not covered!
+
+## 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) {
+
{ i18n.T(ctx, "welcome.hello", i18n.M{"name": name}) }
+}
+
+templ Greeting(person Person) {
+
+ @Hello(person.Name)
+
+}
+```
diff --git a/ctxi18n.go b/ctxi18n.go
index c051180..fd7740a 100644
--- a/ctxi18n.go
+++ b/ctxi18n.go
@@ -26,13 +26,28 @@ var (
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 {
- locales = new(i18n.Locales)
return locales.Load(fs)
}
+// 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) {
diff --git a/ctxi18n_test.go b/ctxi18n_test.go
new file mode 100644
index 0000000..aca987d
--- /dev/null
+++ b/ctxi18n_test.go
@@ -0,0 +1,72 @@
+package ctxi18n_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/invopop/ctxi18n"
+ "github.com/invopop/ctxi18n/i18n"
+ "github.com/invopop/ctxi18n/internal/examples"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDefaults(t *testing.T) {
+ assert.Equal(t, i18n.Code("en"), ctxi18n.DefaultLocale)
+}
+
+func TestLoad(t *testing.T) {
+ err := ctxi18n.Load(examples.Content)
+ assert.NoError(t, err)
+
+ l := ctxi18n.Get("en")
+ assert.NotNil(t, l)
+ assert.Equal(t, "en", l.Code().String())
+}
+
+func TestGet(t *testing.T) {
+ err := ctxi18n.Load(examples.Content)
+ assert.NoError(t, err)
+
+ l := ctxi18n.Get("en")
+ assert.NotNil(t, l)
+ assert.Equal(t, "en", l.Code().String())
+
+ l = ctxi18n.Get("bad")
+ assert.Nil(t, l)
+}
+
+func TestMatch(t *testing.T) {
+ err := ctxi18n.Load(examples.Content)
+ require.NoError(t, err)
+
+ l := ctxi18n.Match("en-US,en;q=0.9,es;q=0.8")
+ assert.NotNil(t, l)
+ assert.Equal(t, "en", l.Code().String())
+}
+
+func TestWithLocale(t *testing.T) {
+ err := ctxi18n.Load(examples.Content)
+ require.NoError(t, err)
+
+ ctx := context.Background()
+ ctx, err = ctxi18n.WithLocale(ctx, "en-US,en;q=0.9,es;q=0.8")
+ require.NoError(t, err)
+
+ l := ctxi18n.Locale(ctx)
+ assert.NotNil(t, l)
+ assert.Equal(t, "en", l.Code().String())
+
+ // Use the default locale if not set
+ ctx, err = ctxi18n.WithLocale(ctx, "inv")
+ assert.NoError(t, err)
+ l = ctxi18n.Locale(ctx)
+ assert.NotNil(t, l)
+ assert.Equal(t, "en", l.Code().String())
+
+ ctxi18n.DefaultLocale = "bad"
+ _, err = ctxi18n.WithLocale(ctx, "inv")
+ assert.ErrorIs(t, err, ctxi18n.ErrMissingLocale)
+ ctxi18n.DefaultLocale = "es"
+
+}
diff --git a/i18n/locales_test.go b/i18n/locales_test.go
index 65b1854..489e4e7 100644
--- a/i18n/locales_test.go
+++ b/i18n/locales_test.go
@@ -29,6 +29,10 @@ func TestLocalesLoad(t *testing.T) {
require.NotNil(t, l)
assert.Equal(t, "en", l.Code().String())
+ l = ls.Match("es-ES,es;q=0.9,en;q=0.8")
+ require.NotNil(t, l)
+ assert.Equal(t, "es", l.Code().String())
+
assert.Nil(t, ls.Match("inv"))
}
diff --git a/templ/i18n/i18n.templ b/templ/i18n/i18n.templ
deleted file mode 100644
index f5a9bf5..0000000
--- a/templ/i18n/i18n.templ
+++ /dev/null
@@ -1,17 +0,0 @@
-package i18n
-
-import (
- "github.com/invopop/ctxi18n/i18n"
-)
-
-// T will use the key to find the translation in the current context and
-// replace the placeholders with the given arguments.
-templ T(key string, args ...any) {
- { i18n.T(ctx, key, args...) }
-}
-
-// N will use the key to find the pluralized translation in the current
-// context and replace the placeholders with the given arguments.
-templ N(key string, n int, args ...any) {
- { i18n.N(ctx, key, n, args...) }
-}
diff --git a/templ/i18n/i18n_templ.go b/templ/i18n/i18n_templ.go
deleted file mode 100644
index e32501e..0000000
--- a/templ/i18n/i18n_templ.go
+++ /dev/null
@@ -1,77 +0,0 @@
-// Code generated by templ - DO NOT EDIT.
-
-// templ: version: v0.2.590
-package i18n
-
-//lint:file-ignore SA4006 This context is only used if a nested component is present.
-
-import "github.com/a-h/templ"
-import "context"
-import "io"
-import "bytes"
-
-import (
- "github.com/invopop/ctxi18n/i18n"
-)
-
-// T will use the key to find the translation in the current context and
-// replace the placeholders with the given arguments.
-func T(key string, args ...any) templ.Component {
- return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
- templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
- if !templ_7745c5c3_IsBuffer {
- templ_7745c5c3_Buffer = templ.GetBuffer()
- defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
- }
- ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var1 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var1 == nil {
- templ_7745c5c3_Var1 = templ.NopComponent
- }
- ctx = templ.ClearChildren(ctx)
- var templ_7745c5c3_Var2 string
- templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.T(ctx, key, args...))
- if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/i18n/i18n.templ`, Line: 9, Col: 28}
- }
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if !templ_7745c5c3_IsBuffer {
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
- }
- return templ_7745c5c3_Err
- })
-}
-
-// N will use the key to find the pluralized translation in the current
-// context and replace the placeholders with the given arguments.
-func N(key string, n int, args ...any) templ.Component {
- return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
- templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
- if !templ_7745c5c3_IsBuffer {
- templ_7745c5c3_Buffer = templ.GetBuffer()
- defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
- }
- ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var3 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var3 == nil {
- templ_7745c5c3_Var3 = templ.NopComponent
- }
- ctx = templ.ClearChildren(ctx)
- var templ_7745c5c3_Var4 string
- templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.N(ctx, key, n, args...))
- if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/i18n/i18n.templ`, Line: 15, Col: 31}
- }
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if !templ_7745c5c3_IsBuffer {
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
- }
- return templ_7745c5c3_Err
- })
-}