├── testdata ├── parser │ ├── en.po │ └── poedit_en_GB.mo ├── structure │ ├── empty.po │ ├── en.mo │ ├── de_AT.mo │ ├── de │ │ ├── c.po │ │ ├── my_category │ │ │ └── b.po │ │ └── LC_MESSAGES │ │ │ ├── z.po │ │ │ └── a.po │ ├── de_AT.po │ ├── base.pot │ └── es │ │ └── helloworld.po ├── humanize │ └── es │ │ └── django.po └── translation-test │ ├── altjson │ ├── de.json │ └── en.json │ ├── json │ ├── de.json │ └── en.json │ └── de │ └── LC_MESSAGES │ ├── z.po │ └── a.po ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── test.yml ├── catalog ├── po │ ├── doc.go │ ├── message.go │ ├── scanner_test.go │ ├── token.go │ ├── wrap.go │ ├── wrap_test.go │ └── message_test.go ├── cldrplural │ ├── generator │ │ ├── README.md │ │ └── json.go │ ├── doc.go │ ├── ast │ │ ├── walk.go │ │ ├── ast.go │ │ └── scanner.go │ ├── builtin.go │ ├── evaluation_test.go │ ├── cldr_test.go │ └── builtin_test.go ├── poplural │ ├── generator │ │ ├── README.md │ │ └── extra.go │ ├── doc.go │ ├── ast │ │ ├── node.go │ │ ├── token.go │ │ ├── compile_test.go │ │ ├── parser_test.go │ │ └── compile.go │ ├── builtin.go │ └── builtin_test.go ├── json_decoder.go ├── json_encoder.go ├── json_decoder_test.go ├── json_encoder_test.go ├── catalog_test.go ├── example_test.go ├── po_decoder_test.go └── po_test.go ├── examples ├── features │ ├── decoder │ │ ├── en.json │ │ ├── es.json │ │ └── README.md │ ├── httptempl │ │ ├── README.md │ │ ├── templates │ │ │ └── home.html │ │ └── not_found.go │ ├── embed │ │ ├── main.go │ │ └── locale │ │ │ ├── de.po │ │ │ ├── es.po │ │ │ └── fr.po │ ├── jhttptempl │ │ ├── locale │ │ │ ├── httptempl.json │ │ │ ├── de.json │ │ │ └── en.json │ │ ├── README.md │ │ ├── templates │ │ │ └── home.html │ │ └── not_found.go │ ├── errors │ │ └── main.go │ ├── loaders │ │ └── main.go │ ├── resolver │ │ └── main.go │ └── printer │ │ └── main.go ├── go.mod ├── locale │ ├── printer.pot │ ├── de │ │ ├── helloworld.po │ │ ├── printer.po │ │ ├── errors.po │ │ └── dayinfo.po │ ├── fr │ │ └── helloworld.po │ ├── errors.pot │ ├── helloworld.pot │ ├── es │ │ └── helloworld.po │ └── dayinfo.pot ├── go.sum ├── helloworld │ └── main.go └── README.md ├── internal ├── util │ ├── compare.go │ └── string_buffer.go ├── cast │ ├── time.go │ ├── number.go │ ├── time_test.go │ └── number_test.go └── calendar │ ├── calendar_test.go │ └── calendar.go ├── .codecov.yml ├── go.mod ├── humanize ├── locale │ ├── af │ │ └── locale.go │ ├── be │ │ └── main.go │ ├── br │ │ └── locale.go │ ├── fy │ │ └── locale.go │ ├── hy │ │ └── locale.go │ ├── ia │ │ └── locale.go │ ├── io │ │ └── locale.go │ ├── kk │ │ └── locale.go │ ├── lb │ │ └── locale.go │ ├── mr │ │ └── locale.go │ ├── my │ │ └── locale.go │ ├── os │ │ └── locale.go │ ├── pa │ │ └── locale.go │ ├── sw │ │ └── locale.go │ ├── tt │ │ └── locale.go │ ├── ur │ │ └── locale.go │ ├── ast │ │ └── main.go │ ├── dsb │ │ └── locale.go │ ├── hsb │ │ └── locale.go │ ├── kab │ │ └── locale.go │ ├── udm │ │ └── locale.go │ ├── esVE │ │ └── locale.go │ ├── mn │ │ └── locale.go │ ├── bg │ │ └── locale.go │ ├── ga │ │ └── locale.go │ ├── et │ │ └── locale.go │ ├── hi │ │ └── locale.go │ ├── kn │ │ └── locale.go │ ├── ta │ │ └── locale.go │ ├── te │ │ └── locale.go │ ├── ar │ │ └── main.go │ ├── is │ │ └── locale.go │ ├── sq │ │ └── locale.go │ ├── bn │ │ └── locale.go │ ├── bs │ │ └── locale.go │ ├── km │ │ └── locale.go │ ├── gd │ │ └── locale.go │ ├── he │ │ └── locale.go │ ├── ja │ │ └── locale.go │ ├── ko │ │ └── locale.go │ ├── ug │ │ └── locale.go │ ├── cy │ │ └── locale.go │ ├── de │ │ └── locale.go │ ├── el │ │ └── locale.go │ ├── ig │ │ └── locale.go │ ├── ml │ │ └── locale.go │ ├── ms │ │ └── locale.go │ ├── az │ │ └── main.go │ ├── cs │ │ └── locale.go │ ├── da │ │ └── locale.go │ ├── enAU │ │ └── locale.go │ ├── enGB │ │ └── locale.go │ ├── fr │ │ └── locale.go │ ├── id │ │ └── locale.go │ ├── it │ │ └── locale.go │ ├── lv │ │ └── locale.go │ ├── mk │ │ └── locale.go │ ├── nb │ │ └── locale.go │ ├── ne │ │ └── locale.go │ ├── nl │ │ └── locale.go │ ├── nn │ │ └── locale.go │ ├── pl │ │ └── locale.go │ ├── ro │ │ └── locale.go │ ├── sk │ │ └── locale.go │ ├── sv │ │ └── locale.go │ ├── th │ │ └── locale.go │ ├── tr │ │ └── locale.go │ ├── arDZ │ │ └── main.go │ ├── fa │ │ └── locale.go │ ├── hr │ │ └── locale.go │ ├── hu │ │ └── locale.go │ ├── sl │ │ └── locale.go │ ├── sr │ │ └── locale.go │ ├── uk │ │ └── locale.go │ ├── ckb │ │ └── locale.go │ ├── esAR │ │ └── locale.go │ ├── ka │ │ └── locale.go │ ├── ky │ │ └── locale.go │ ├── ru │ │ └── locale.go │ ├── tg │ │ └── locale.go │ ├── tk │ │ └── locale.go │ ├── fi │ │ └── locale.go │ ├── vi │ │ └── locale.go │ ├── srLatn │ │ └── locale.go │ ├── uz │ │ └── locale.go │ ├── zhHans │ │ └── locale.go │ ├── zhHant │ │ └── locale.go │ ├── ca │ │ └── locale.go │ ├── eu │ │ └── locale.go │ ├── es │ │ └── locale.go │ ├── lt │ │ └── locale.go │ ├── gl │ │ └── locale.go │ ├── pt │ │ └── locale.go │ ├── eo │ │ └── locale.go │ ├── ptBR │ │ └── locale.go │ ├── esCO │ │ └── locale.go │ ├── esMX │ │ └── locale.go │ └── locale_test.go ├── doc.go ├── locale.go ├── locale_test.go ├── humanizer_test.go └── dates.go ├── localize ├── error.go └── types.go ├── .gitignore ├── Makefile ├── go.sum ├── .golangci.yml ├── example_test.go ├── LICENSE └── doc.go /testdata/parser/en.po: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/structure/empty.po: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | #### Summary -------------------------------------------------------------------------------- /testdata/humanize/es/django.po: -------------------------------------------------------------------------------- 1 | this file is an invalid po file 2 | for tests only -------------------------------------------------------------------------------- /catalog/po/doc.go: -------------------------------------------------------------------------------- 1 | // Package po allows to read and write gettext po files. 2 | package po 3 | -------------------------------------------------------------------------------- /testdata/structure/en.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vorlif/spreak/HEAD/testdata/structure/en.mo -------------------------------------------------------------------------------- /testdata/structure/de_AT.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vorlif/spreak/HEAD/testdata/structure/de_AT.mo -------------------------------------------------------------------------------- /testdata/parser/poedit_en_GB.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vorlif/spreak/HEAD/testdata/parser/poedit_en_GB.mo -------------------------------------------------------------------------------- /examples/features/decoder/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello.world": "Hello world", 3 | "dog": "I have %d dog", 4 | "dog_plural": "I have %d dogs" 5 | } -------------------------------------------------------------------------------- /examples/features/decoder/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello.world": "Hola Mundo", 3 | "dog": "Tengo %d perro", 4 | "dog_plural": "Tengo %d perros" 5 | } -------------------------------------------------------------------------------- /internal/util/compare.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "math" 4 | 5 | func FloatEqual(a, b float64) bool { 6 | return math.Abs(a-b) <= 1e-6 7 | } 8 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | ignore: 10 | - "humanize/locale" -------------------------------------------------------------------------------- /examples/features/decoder/README.md: -------------------------------------------------------------------------------- 1 | ### Custom decoder 2 | 3 | The following example shows how to implement a custom decoder to read JSON files and use them for translation. 4 | To use the built-in JSON decoder, see [jhttptempl](../jhttptempl). -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | -------------------------------------------------------------------------------- /catalog/cldrplural/generator/README.md: -------------------------------------------------------------------------------- 1 | # How to create the built-in plural functions? 2 | 3 | 1. Go to https://github.com/unicode-org/cldr-json/blob/41.0.0/cldr-json/cldr-core/supplemental/plurals.json 4 | 2. Select the appropriate version from the tags 5 | 3. Copy the file to this directory 6 | 4. Run `go run .` -------------------------------------------------------------------------------- /catalog/poplural/generator/README.md: -------------------------------------------------------------------------------- 1 | # How to create the built-in plural functions? 2 | 3 | 1. Clone https://github.com/php-gettext/Languages 4 | 2. Execute ./bin/export-plural-rules prettyjson --output=plurals.json --parenthesis=yes --reduce=no 5 | 3. Copy the file `plurals.json` to this directory 6 | 4. Run `go run .` -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module example.com/spreak 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/Xuanwo/go-locale v1.1.3 7 | github.com/vorlif/spreak v0.0.0 8 | golang.org/x/text v0.29.0 9 | ) 10 | 11 | require golang.org/x/sys v0.28.0 // indirect 12 | 13 | replace github.com/vorlif/spreak v0.0.0 => ../ 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vorlif/spreak 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/stretchr/testify v1.11.1 7 | golang.org/x/text v0.28.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /catalog/cldrplural/doc.go: -------------------------------------------------------------------------------- 1 | // Package cldrplural provides support for using CLDR plural rules in text. 2 | // 3 | // NOTE: The following package are subject to change and are not intended primarily for official use. 4 | // However, they can be helpful if you want to implement your own catalog. 5 | // So use them at your own risk. 6 | package cldrplural 7 | -------------------------------------------------------------------------------- /catalog/poplural/doc.go: -------------------------------------------------------------------------------- 1 | // Package poplural provides support for using gettext plural forms in text. 2 | // 3 | // NOTE: The following package are subject to change and are not intended primarily for official use. 4 | // However, they can be helpful if you want to implement your own catalog. 5 | // So use them at your own risk. 6 | package poplural 7 | -------------------------------------------------------------------------------- /humanize/locale/af/locale.go: -------------------------------------------------------------------------------- 1 | package af 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("af"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/be/main.go: -------------------------------------------------------------------------------- 1 | package be 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("be"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/br/locale.go: -------------------------------------------------------------------------------- 1 | package br 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("br"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/fy/locale.go: -------------------------------------------------------------------------------- 1 | package fy 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("fy"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/hy/locale.go: -------------------------------------------------------------------------------- 1 | package hy 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("hy"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/ia/locale.go: -------------------------------------------------------------------------------- 1 | package ia 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ia"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/io/locale.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("io"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/kk/locale.go: -------------------------------------------------------------------------------- 1 | package kk 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("kk"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/lb/locale.go: -------------------------------------------------------------------------------- 1 | package lb 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("lb"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/mr/locale.go: -------------------------------------------------------------------------------- 1 | package mr 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("mr"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/my/locale.go: -------------------------------------------------------------------------------- 1 | package my 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("my"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/os/locale.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("os"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/pa/locale.go: -------------------------------------------------------------------------------- 1 | package pa 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("pa"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/sw/locale.go: -------------------------------------------------------------------------------- 1 | package sw 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("sw"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/tt/locale.go: -------------------------------------------------------------------------------- 1 | package tt 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("tt"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/ur/locale.go: -------------------------------------------------------------------------------- 1 | package ur 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ur"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/ast/main.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ast"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/dsb/locale.go: -------------------------------------------------------------------------------- 1 | package dsb 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("dsb"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/hsb/locale.go: -------------------------------------------------------------------------------- 1 | package hsb 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("hsb"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/kab/locale.go: -------------------------------------------------------------------------------- 1 | package kab 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("kab"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/udm/locale.go: -------------------------------------------------------------------------------- 1 | package udm 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("udm"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /humanize/locale/esVE/locale.go: -------------------------------------------------------------------------------- 1 | package esVE 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("es-VE"), 17 | Fs: fsys, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /localize/error.go: -------------------------------------------------------------------------------- 1 | package localize 2 | 3 | // Error is returned when a Localizer or Locale translates an error into the target language. 4 | type Error struct { 5 | Translation string 6 | Wrapped error 7 | } 8 | 9 | func (e *Error) Error() string { return e.Translation } 10 | 11 | func (e *Error) Unwrap() error { return e.Wrapped } 12 | 13 | func (e *Error) String() string { return e.Translation } 14 | -------------------------------------------------------------------------------- /humanize/locale/mn/locale.go: -------------------------------------------------------------------------------- 1 | package mn 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("mn"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "d F Y", 20 | TimeFormat: "g:i A", 21 | ShortDateFormat: "j M Y", 22 | }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/translation-test/altjson/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain.name": "Alternativer Domain", 3 | "user.welcome": "Willkommen %s", 4 | "week": { 5 | "one": "Woche", 6 | "other": "Wochen" 7 | }, 8 | "days": { 9 | "one": "%d Tag", 10 | "other": "%d Tage" 11 | }, 12 | "test_context": { 13 | "context": "context", 14 | "other": "Test mit Context" 15 | }, 16 | "result_context": { 17 | "context": "context", 18 | "one": "%d Ergebnis", 19 | "other": "%d Ergebnisse" 20 | } 21 | } -------------------------------------------------------------------------------- /humanize/locale/bg/locale.go: -------------------------------------------------------------------------------- 1 | package bg 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("bg"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "d F Y", 20 | TimeFormat: "H:i", 21 | MonthDayFormat: "j F", 22 | ShortDateFormat: "d.m.Y", 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /humanize/locale/ga/locale.go: -------------------------------------------------------------------------------- 1 | package ga 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ga"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "H:i", 21 | MonthDayFormat: "j F", 22 | ShortDateFormat: "j M Y", 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /humanize/locale/et/locale.go: -------------------------------------------------------------------------------- 1 | package et 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("et"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. F Y", 20 | TimeFormat: "G:i", 21 | MonthDayFormat: "j. F", 22 | ShortDateFormat: "d.m.Y", 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /humanize/locale/hi/locale.go: -------------------------------------------------------------------------------- 1 | package hi 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("hi"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "g:i A", 21 | MonthDayFormat: "j F", 22 | ShortDateFormat: "d-m-Y", 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /humanize/locale/kn/locale.go: -------------------------------------------------------------------------------- 1 | package kn 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("kn"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "h:i A", 21 | MonthDayFormat: "j F", 22 | ShortDateFormat: "j M Y", 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /humanize/locale/ta/locale.go: -------------------------------------------------------------------------------- 1 | package ta 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ta"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F, Y", 20 | TimeFormat: "g:i A", 21 | MonthDayFormat: "j F", 22 | ShortDateFormat: "j M, Y", 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /humanize/locale/te/locale.go: -------------------------------------------------------------------------------- 1 | package te 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("te"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "g:i A", 21 | MonthDayFormat: "j F", 22 | ShortDateFormat: "j M Y", 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Summary 11 | 12 | Bug report in one concise sentence 13 | 14 | #### Steps to reproduce 15 | 16 | How can we reproduce the issue (what version are you using?) 17 | 18 | #### Expected behavior 19 | 20 | Describe your issue in detail 21 | 22 | #### Observed behavior (that appears unintentional) 23 | 24 | What did you see happen? Please include relevant error messages. 25 | -------------------------------------------------------------------------------- /testdata/translation-test/json/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.name": "TODO Liste", 3 | "date.weekday": "Wochentag", 4 | "user.goodbye": "Tschau %s", 5 | "animal.cat": { 6 | "one": "", 7 | "other": "" 8 | }, 9 | "animal.dog_my-animals": { 10 | "context": "my-animals", 11 | "one": "Ich habe einen Hund", 12 | "other": "Ich habe Hunde" 13 | }, 14 | "animal.dog": { 15 | "one": "Jemand hat einen Hund", 16 | "other": "Jemand hat Hunde" 17 | }, 18 | "missing.plural": { 19 | "one": "", 20 | "other": "Singular wird vermisst" 21 | } 22 | } -------------------------------------------------------------------------------- /humanize/locale/ar/main.go: -------------------------------------------------------------------------------- 1 | package ar 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ar"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F، Y", 20 | TimeFormat: "g:i A", 21 | YearMonthFormat: "F Y", 22 | MonthDayFormat: "j F", 23 | ShortDateFormat: "d/m/Y", 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /humanize/locale/is/locale.go: -------------------------------------------------------------------------------- 1 | package is 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("is"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. F Y", 20 | TimeFormat: "H:i", 21 | YearMonthFormat: "F Y", 22 | MonthDayFormat: "j. F", 23 | ShortDateFormat: "j.n.Y", 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /humanize/locale/sq/locale.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("sq"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "d F Y", 20 | TimeFormat: "g.i.A", 21 | YearMonthFormat: "F Y", 22 | MonthDayFormat: "j F", 23 | ShortDateFormat: "Y-m-d", 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/cast/time.go: -------------------------------------------------------------------------------- 1 | package cast 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | func ToTime(i any) (t time.Time, err error) { 10 | i = Indirect(i) 11 | if i == nil { 12 | return time.Time{}, errors.New("time is nil") 13 | } 14 | 15 | switch v := i.(type) { 16 | case time.Time: 17 | return v, nil 18 | case time.Duration: 19 | return time.Now().Add(v), nil 20 | } 21 | 22 | if num, err := ToNumber(i); err == nil { 23 | return time.Unix(int64(num), 0), nil 24 | } 25 | return time.Time{}, fmt.Errorf("unable to cast %#v of type %T to Time", i, i) 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cover.out 2 | coverage.html 3 | 4 | catalog/cldrplural/generator/plurals.json 5 | catalog/poplural/generator/plurals.json 6 | 7 | # IDE paths 8 | .project 9 | .settings 10 | .buildpath 11 | .idea 12 | 13 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 14 | *.o 15 | *.a 16 | *.so 17 | 18 | # Folders 19 | _obj 20 | _test 21 | 22 | # Architecture specific extensions/prefixes 23 | *.[568vq] 24 | [568vq].out 25 | 26 | *.cgo1.go 27 | *.cgo2.c 28 | _cgo_defun.c 29 | _cgo_gotypes.go 30 | _cgo_export.* 31 | 32 | _testmain.go 33 | 34 | *.exe 35 | *.test 36 | *.prof 37 | -------------------------------------------------------------------------------- /humanize/locale/bn/locale.go: -------------------------------------------------------------------------------- 1 | package bn 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("bn"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F, Y", 20 | TimeFormat: "g:i A", 21 | YearMonthFormat: "F Y", 22 | MonthDayFormat: "j F", 23 | ShortDateFormat: "j M, Y", 24 | FirstDayOfWeek: 6, 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testdata/translation-test/altjson/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain.name": "alternative domain", 3 | "user.welcome": "Welcome %s", 4 | "week": { 5 | "one": "week", 6 | "other": "weeks" 7 | }, 8 | "days": { 9 | "one": "%d day", 10 | "other": "%d days" 11 | }, 12 | "test_context": { 13 | "context": "context", 14 | "other": "test with context" 15 | }, 16 | "result_context": { 17 | "context": "context", 18 | "one": "%d result", 19 | "other": "%d results" 20 | }, 21 | "user.welcome_cowboy": { 22 | "context": "cowboy", 23 | "other": "Howdy %s" 24 | } 25 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | gofmt: 4 | gofmt -s -w . 5 | goimports -w -local github.com/vorlif/spreak ./ 6 | 7 | 8 | lint: 9 | @echo Running golangci-lint 10 | golangci-lint run --fix ./... 11 | 12 | 13 | test-race: 14 | go test -race -run=. ./... || exit 1; 15 | 16 | 17 | test: 18 | go test -short ./... 19 | 20 | 21 | coverage: 22 | go test -short -v -coverprofile cover.out ./... 23 | go tool cover -func cover.out 24 | go tool cover -html=cover.out -o coverage.html 25 | 26 | 27 | clean: 28 | @echo Cleaning 29 | 30 | go clean -i ./... 31 | rm -f cover.out 32 | rm -f coverage.html 33 | rm -rf dist 34 | -------------------------------------------------------------------------------- /humanize/locale/bs/locale.go: -------------------------------------------------------------------------------- 1 | package bs 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("bs"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. N Y.", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "j. N. Y. G:i T", 22 | YearMonthFormat: "F Y.", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "Y M j", 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /catalog/poplural/ast/node.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | type Node interface { 4 | Type() Token 5 | } 6 | 7 | type OperandExpr struct{} 8 | 9 | type ValueExpr struct { 10 | Value int64 11 | } 12 | 13 | type BinaryExpr struct { 14 | X Node 15 | Op Token // and, or, ==, != 16 | Y Node 17 | } 18 | 19 | type QuestionMarkExpr struct { 20 | Cond Node 21 | T Node 22 | F Node 23 | } 24 | 25 | func (OperandExpr) Type() Token { return Operand } 26 | func (ValueExpr) Type() Token { return Value } 27 | func (QuestionMarkExpr) Type() Token { return Question } 28 | func (e BinaryExpr) Type() Token { return e.Op } 29 | -------------------------------------------------------------------------------- /humanize/locale/km/locale.go: -------------------------------------------------------------------------------- 1 | package km 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("km"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j ខែ F ឆ្នាំ Y", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "j ខែ F ឆ្នាំ Y, G:i", 22 | MonthDayFormat: "j F", 23 | ShortDateFormat: "j M Y", 24 | ShortDatetimeFormat: "j M Y, G:i", 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /humanize/locale/gd/locale.go: -------------------------------------------------------------------------------- 1 | package gd 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("gd"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "h:ia", 21 | DateTimeFormat: "j F Y h:ia", 22 | MonthDayFormat: "j F", 23 | ShortDateFormat: "j M Y", 24 | ShortDatetimeFormat: "j M Y h:ia", 25 | FirstDayOfWeek: 1, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /humanize/locale/he/locale.go: -------------------------------------------------------------------------------- 1 | package he 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("he"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j בF Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j בF Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j בF", 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y H:i", 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /humanize/locale/ja/locale.go: -------------------------------------------------------------------------------- 1 | package ja 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ja"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "Y年n月j日", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "Y年n月j日G:i", 22 | YearMonthFormat: "Y年n月", 23 | MonthDayFormat: "n月j日", 24 | ShortDateFormat: "Y/m/d", 25 | ShortDatetimeFormat: "Y/m/d G:i", 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /catalog/json_decoder.go: -------------------------------------------------------------------------------- 1 | package catalog 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "golang.org/x/text/language" 7 | ) 8 | 9 | // JSONDecoder is a Decoder for reading JSON catalog files. 10 | type JSONDecoder struct{} 11 | 12 | var _ Decoder = (*JSONDecoder)(nil) 13 | 14 | // NewJSONDecoder returns a new Decoder for reading JSON catalog files. 15 | func NewJSONDecoder() *JSONDecoder { return &JSONDecoder{} } 16 | 17 | func (JSONDecoder) Decode(lang language.Tag, domain string, data []byte) (Catalog, error) { 18 | cat := NewJSONCatalog(lang, domain) 19 | if err := json.Unmarshal(data, &cat); err != nil { 20 | return nil, err 21 | } 22 | 23 | return cat, nil 24 | } 25 | -------------------------------------------------------------------------------- /humanize/locale/ko/locale.go: -------------------------------------------------------------------------------- 1 | package ko 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ko"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "Y년 n월 j일", 20 | TimeFormat: "A g:i", 21 | DateTimeFormat: "Y년 n월 j일 g:i A", 22 | YearMonthFormat: "Y년 n월", 23 | MonthDayFormat: "n월 j일", 24 | ShortDateFormat: "Y-n-j.", 25 | ShortDatetimeFormat: "Y-n-j H:i", 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /humanize/locale/ug/locale.go: -------------------------------------------------------------------------------- 1 | package ug 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ug"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F, Y", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "Y/m/d", 25 | ShortDatetimeFormat: "Y/m/d G:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/cy/locale.go: -------------------------------------------------------------------------------- 1 | package cy 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("cy"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "P", 21 | DateTimeFormat: "j F Y, P", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y P", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/de/locale.go: -------------------------------------------------------------------------------- 1 | package de 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.German, 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j. F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/el/locale.go: -------------------------------------------------------------------------------- 1 | package el 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("el"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "d/m/Y", 20 | TimeFormat: "P", 21 | DateTimeFormat: "d/m/Y P", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y P", 26 | FirstDayOfWeek: 0, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/ig/locale.go: -------------------------------------------------------------------------------- 1 | package ig 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ig"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "P", 21 | DateTimeFormat: "j F Y P", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/ml/locale.go: -------------------------------------------------------------------------------- 1 | package ml 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ml"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "N j, Y", 20 | TimeFormat: "P", 21 | DateTimeFormat: "N j, Y, P", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "F j", 24 | ShortDateFormat: "m/d/Y", 25 | ShortDatetimeFormat: "m/d/Y P", 26 | FirstDayOfWeek: 0, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/ms/locale.go: -------------------------------------------------------------------------------- 1 | package ms 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ms"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j M Y", 20 | TimeFormat: "P", 21 | DateTimeFormat: "j M Y, P", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y P", 26 | FirstDayOfWeek: 0, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/az/main.go: -------------------------------------------------------------------------------- 1 | package az 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("az"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j E Y", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "j E Y, G:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/cs/locale.go: -------------------------------------------------------------------------------- 1 | package cs 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("cs"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. E Y", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "j. E Y G:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y G:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/da/locale.go: -------------------------------------------------------------------------------- 1 | package da 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("da"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j. F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/enAU/locale.go: -------------------------------------------------------------------------------- 1 | package enAU 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("en-AU"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j M Y", 20 | TimeFormat: "P", 21 | DateTimeFormat: "j M Y, P", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y P", 26 | FirstDayOfWeek: 0, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/enGB/locale.go: -------------------------------------------------------------------------------- 1 | package enGB 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("en-GB"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j M Y", 20 | TimeFormat: "P", 21 | DateTimeFormat: "j M Y, P", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y P", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/fr/locale.go: -------------------------------------------------------------------------------- 1 | package fr 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("fr"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/id/locale.go: -------------------------------------------------------------------------------- 1 | package id 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("id"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j N Y", 20 | DateTimeFormat: "j N Y, G.i", 21 | TimeFormat: "G.i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d-m-Y", 25 | ShortDatetimeFormat: "d-m-Y G.i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/it/locale.go: -------------------------------------------------------------------------------- 1 | package it 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("it"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "d F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "l d F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/lv/locale.go: -------------------------------------------------------------------------------- 1 | package lv 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("lv"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j. F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/mk/locale.go: -------------------------------------------------------------------------------- 1 | package mk 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("mk"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "d F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j. F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "j.m.Y", 25 | ShortDatetimeFormat: "j.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/nb/locale.go: -------------------------------------------------------------------------------- 1 | package nb 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("nb"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j. F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/ne/locale.go: -------------------------------------------------------------------------------- 1 | package ne 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ne"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j. F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/nl/locale.go: -------------------------------------------------------------------------------- 1 | package nl 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("nl"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "j-n-Y", 25 | ShortDatetimeFormat: "j-n-Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/nn/locale.go: -------------------------------------------------------------------------------- 1 | package nn 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("nn"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j. F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/pl/locale.go: -------------------------------------------------------------------------------- 1 | package pl 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("pl"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j E Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j E Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j E", 24 | ShortDateFormat: "d-m-Y", 25 | ShortDatetimeFormat: "d-m-Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/ro/locale.go: -------------------------------------------------------------------------------- 1 | package ro 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ro"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j F Y, H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y, H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/sk/locale.go: -------------------------------------------------------------------------------- 1 | package sk 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("sk"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. F Y", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "j. F Y G:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y G:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/sv/locale.go: -------------------------------------------------------------------------------- 1 | package sv 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("sv"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "Y-m-d", 25 | ShortDatetimeFormat: "Y-m-d H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/th/locale.go: -------------------------------------------------------------------------------- 1 | package th 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("th"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "j F Y, G:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "j M Y", 25 | ShortDatetimeFormat: "j M Y, G:i", 26 | FirstDayOfWeek: 0, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/tr/locale.go: -------------------------------------------------------------------------------- 1 | package tr 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("tr"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "d F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "d F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "d F", 24 | ShortDateFormat: "d M Y", 25 | ShortDatetimeFormat: "d M Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/arDZ/main.go: -------------------------------------------------------------------------------- 1 | package arDZ 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ar-DZ"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j F Y H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "j F Y", 25 | ShortDatetimeFormat: "j F Y H:i", 26 | FirstDayOfWeek: 0, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/fa/locale.go: -------------------------------------------------------------------------------- 1 | package fa 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("fa"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "j F Y، ساعت G:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "Y/n/j", 25 | ShortDatetimeFormat: "Y/n/j، G:i", 26 | FirstDayOfWeek: 6, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/hr/locale.go: -------------------------------------------------------------------------------- 1 | package hr 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("hr"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. E Y.", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j. E Y. H:i", 22 | YearMonthFormat: "F Y.", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "j.m.Y.", 25 | ShortDatetimeFormat: "j.m.Y. H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/hu/locale.go: -------------------------------------------------------------------------------- 1 | package hu 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("hu"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "Y. F j.", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "Y. F j. H:i", 22 | YearMonthFormat: "Y. F", 23 | MonthDayFormat: "F j.", 24 | ShortDateFormat: "Y.m.d.", 25 | ShortDatetimeFormat: "Y.m.d. H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/sl/locale.go: -------------------------------------------------------------------------------- 1 | package sl 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("sl"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "d. F Y", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j. F Y. H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "j. M. Y", 25 | ShortDatetimeFormat: "j.n.Y. H:i", 26 | FirstDayOfWeek: 0, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/sr/locale.go: -------------------------------------------------------------------------------- 1 | package sr 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("sr"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. F Y.", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j. F Y. H:i", 22 | YearMonthFormat: "F Y.", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "j.m.Y.", 25 | ShortDatetimeFormat: "j.m.Y. H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/uk/locale.go: -------------------------------------------------------------------------------- 1 | package uk 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("uk"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "d E Y р.", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "d E Y р. H:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "d F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/ckb/locale.go: -------------------------------------------------------------------------------- 1 | package ckb 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ckb"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j F Y", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "j F Y، کاتژمێر G:i", 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "Y/n/j", 25 | ShortDatetimeFormat: "Y/n/j، G:i", 26 | FirstDayOfWeek: 6, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/esAR/locale.go: -------------------------------------------------------------------------------- 1 | package esAR 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("es-AR"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `j N Y`, 20 | TimeFormat: `H:i`, 21 | DateTimeFormat: `j N Y H:i`, 22 | YearMonthFormat: `F Y`, 23 | MonthDayFormat: `j \d\e F`, 24 | ShortDateFormat: `d/m/Y`, 25 | ShortDatetimeFormat: `d/m/Y H:i`, 26 | FirstDayOfWeek: 0, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/ka/locale.go: -------------------------------------------------------------------------------- 1 | package ka 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ka"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "l, j F, Y", 20 | TimeFormat: "h:i a", 21 | DateTimeFormat: "j F, Y h:i a", 22 | YearMonthFormat: "F, Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "j.M.Y", 25 | ShortDatetimeFormat: "j.M.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/ky/locale.go: -------------------------------------------------------------------------------- 1 | package ky 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ky"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j E Y ж.", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "j E Y ж. G:i", 22 | YearMonthFormat: "F Y ж.", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/ru/locale.go: -------------------------------------------------------------------------------- 1 | package ru 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ru"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j E Y г.", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "j E Y г. G:i", 22 | YearMonthFormat: "F Y г.", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/tg/locale.go: -------------------------------------------------------------------------------- 1 | package tg 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("tg"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j E Y г.", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "j E Y г. G:i", 22 | YearMonthFormat: "F Y г.", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/tk/locale.go: -------------------------------------------------------------------------------- 1 | package tk 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("tk"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j E Y г.", 20 | TimeFormat: "G:i", 21 | DateTimeFormat: "j E Y г. G:i", 22 | YearMonthFormat: "F Y г.", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/fi/locale.go: -------------------------------------------------------------------------------- 1 | package fi 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("fi"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. E Y", 20 | TimeFormat: "G.i", 21 | DateTimeFormat: `j. E Y \k\e\l\l\o G.i`, 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "j.n.Y", 25 | ShortDatetimeFormat: "j.n.Y G.i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/vi/locale.go: -------------------------------------------------------------------------------- 1 | package vi 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("vi"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `\N\gà\y d \t\há\n\g n \nă\m Y`, 20 | TimeFormat: "H:i", 21 | DateTimeFormat: `:i \N\gà\y d \t\há\n\g n \nă\m Y`, 22 | YearMonthFormat: "F Y", 23 | MonthDayFormat: "j F", 24 | ShortDateFormat: "d-m-Y", 25 | ShortDatetimeFormat: "H:i d-m-Y", 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /humanize/locale/srLatn/locale.go: -------------------------------------------------------------------------------- 1 | package srLatn 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("sr-Latn"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "j. F Y.", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "j. F Y. H:i", 22 | YearMonthFormat: "F Y.", 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: "j.m.Y.", 25 | ShortDatetimeFormat: "j.m.Y. H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/uz/locale.go: -------------------------------------------------------------------------------- 1 | package uz 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("uz"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `j-E, Y-\y\i\l`, 20 | TimeFormat: "G:i", 21 | DateTimeFormat: `j-E, Y-\y\i\l G:i`, 22 | YearMonthFormat: `F Y-\y\i\l`, 23 | MonthDayFormat: "j-E", 24 | ShortDateFormat: "d.m.Y", 25 | ShortDatetimeFormat: "d.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/zhHans/locale.go: -------------------------------------------------------------------------------- 1 | package zhHans 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("zh-Hans"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "Y年n月j日", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "Y年n月j日 H:i", 22 | YearMonthFormat: "Y年n月", 23 | MonthDayFormat: "m月j日", 24 | ShortDateFormat: "Y年n月j日", 25 | ShortDatetimeFormat: "Y年n月j日 H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/zhHant/locale.go: -------------------------------------------------------------------------------- 1 | package zhHant 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("zh-Hant"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: "Y年n月j日", 20 | TimeFormat: "H:i", 21 | DateTimeFormat: "Y年n月j日 H:i", 22 | YearMonthFormat: "Y年n月", 23 | MonthDayFormat: "m月j日", 24 | ShortDateFormat: "Y年n月j日", 25 | ShortDatetimeFormat: "Y年n月j日 H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/ca/locale.go: -------------------------------------------------------------------------------- 1 | package ca 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("ca"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `j E \d\e Y`, 20 | TimeFormat: "G:i", 21 | DateTimeFormat: `j E \d\e Y \a \l\e\s G:i`, 22 | YearMonthFormat: `F \d\e\l Y`, 23 | MonthDayFormat: "j E", 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y G:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/eu/locale.go: -------------------------------------------------------------------------------- 1 | package eu 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("eu"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `Y\k\o N j\a`, 20 | TimeFormat: "H:i", 21 | DateTimeFormat: `Y\k\o N j\a, H:i`, 22 | YearMonthFormat: `Y\k\o F`, 23 | MonthDayFormat: `F\r\e\n j\a`, 24 | ShortDateFormat: "Y-m-d", 25 | ShortDatetimeFormat: "Y-m-d H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/es/locale.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.Spanish, 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `j \d\e F \d\e Y`, 20 | TimeFormat: "H:i", 21 | DateTimeFormat: `j \d\e F \d\e Y \a \l\a\s H:i`, 22 | YearMonthFormat: `F \d\e Y`, 23 | MonthDayFormat: `j \d\e F`, 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/lt/locale.go: -------------------------------------------------------------------------------- 1 | package lv 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("lv"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `Y. \g\a\d\a j. F`, 20 | TimeFormat: "H:i", 21 | DateTimeFormat: `Y. \g\a\d\a j. F, H:i`, 22 | YearMonthFormat: `Y. \g. F`, 23 | MonthDayFormat: "j. F", 24 | ShortDateFormat: `j.m.Y`, 25 | ShortDatetimeFormat: "j.m.Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/gl/locale.go: -------------------------------------------------------------------------------- 1 | package gl 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("gl"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `j \d\e F \d\e Y`, 20 | TimeFormat: "H:i", 21 | DateTimeFormat: `j \d\e F \d\e Y \á\s H:i`, 22 | YearMonthFormat: `F \d\e Y`, 23 | MonthDayFormat: `j \d\e F`, 24 | ShortDateFormat: "d-m-Y", 25 | ShortDatetimeFormat: "d-m-Y, H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/pt/locale.go: -------------------------------------------------------------------------------- 1 | package pt 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("pt"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `j \d\e F \d\e Y`, 20 | TimeFormat: "H:i", 21 | DateTimeFormat: `j \d\e F \d\e Y à\s H:i`, 22 | YearMonthFormat: `F \d\e Y`, 23 | MonthDayFormat: `j \d\e F`, 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y H:i", 26 | FirstDayOfWeek: 0, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/eo/locale.go: -------------------------------------------------------------------------------- 1 | package eo 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("eo"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `j\-\a \d\e F Y`, 20 | TimeFormat: "H:i", 21 | DateTimeFormat: `j\-\a \d\e F Y\, \j\e H:i`, 22 | YearMonthFormat: `F \d\e Y`, 23 | MonthDayFormat: `j\-\a \d\e F`, 24 | ShortDateFormat: `Y-m-d`, 25 | ShortDatetimeFormat: `Y-m-d H:i`, 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/ptBR/locale.go: -------------------------------------------------------------------------------- 1 | package ptBR 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("pt-BR"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `j \d\e F \d\e Y`, 20 | TimeFormat: "H:i", 21 | DateTimeFormat: `j \d\e F \d\e Y à\s H:i`, 22 | YearMonthFormat: `F \d\e Y`, 23 | MonthDayFormat: `j \d\e F`, 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y H:i", 26 | FirstDayOfWeek: 0, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/esCO/locale.go: -------------------------------------------------------------------------------- 1 | package esCO 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("es-CO"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `j \d\e F \d\e Y`, 20 | TimeFormat: "H:i", 21 | DateTimeFormat: `j \d\e F \d\e Y \a \l\a\s H:i`, 22 | YearMonthFormat: `F \d\e Y`, 23 | MonthDayFormat: `j \d\e F`, 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /humanize/locale/esMX/locale.go: -------------------------------------------------------------------------------- 1 | package esMX 2 | 3 | import ( 4 | "embed" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak/humanize" 9 | ) 10 | 11 | //go:embed *.po 12 | var fsys embed.FS 13 | 14 | func New() *humanize.LocaleData { 15 | return &humanize.LocaleData{ 16 | Lang: language.MustParse("es-MX"), 17 | Fs: fsys, 18 | Format: &humanize.FormatData{ 19 | DateFormat: `j \d\e F \d\e Y`, 20 | TimeFormat: "H:i", 21 | DateTimeFormat: `j \d\e F \d\e Y \a \l\a\s H:i`, 22 | YearMonthFormat: `F \d\e Y`, 23 | MonthDayFormat: `j \d\e F`, 24 | ShortDateFormat: "d/m/Y", 25 | ShortDatetimeFormat: "d/m/Y H:i", 26 | FirstDayOfWeek: 1, 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/util/string_buffer.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "bytes" 4 | 5 | type StringBuffer struct { 6 | buff bytes.Buffer 7 | len int 8 | } 9 | 10 | func (b *StringBuffer) Len() int { 11 | return b.len 12 | } 13 | 14 | func (b *StringBuffer) Reset() { 15 | b.buff.Reset() 16 | b.len = 0 17 | } 18 | 19 | func (b *StringBuffer) WriteInto(w *StringBuffer) { 20 | _, _ = b.buff.WriteTo(&w.buff) 21 | w.len += b.len 22 | b.len = 0 23 | } 24 | 25 | func (b *StringBuffer) String() string { 26 | return b.buff.String() 27 | } 28 | 29 | func (b *StringBuffer) WriteRune(r rune) { 30 | b.buff.WriteRune(r) 31 | b.len++ 32 | } 33 | 34 | func (b *StringBuffer) WriteString(s string) { 35 | _, _ = b.buff.WriteString(s) 36 | b.len += len(s) 37 | } 38 | -------------------------------------------------------------------------------- /humanize/doc.go: -------------------------------------------------------------------------------- 1 | // Package humanize provides a collection of functions to convert Go data structures into a human-readable format. 2 | // 3 | // It was adopted in large part by the Django project and is therefore able to translate into several languages. 4 | // A list of all supported languages can be found in the locale package. 5 | // 6 | // # Usage 7 | // 8 | // Create a collection with the languages you want to use. 9 | // 10 | // collection := humanize.MustNew( 11 | // humanize.WithLocale(es.New(), ar.New(), zhHans.New()), 12 | // ) 13 | // 14 | // Create a humanizer 15 | // 16 | // h := collection.CreateHumanizer(language.Spanish) 17 | // 18 | // Use it 19 | // 20 | // fmt.Println(h.Intword(1_000_000_000)) 21 | // // Output: 1,0 millardo 22 | package humanize 23 | -------------------------------------------------------------------------------- /testdata/structure/de/c.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the Poedit package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Project 1.0\n" 9 | "Report-Msgid-Bugs-To: help@example.com\n" 10 | "POT-Creation-Date: 2021-06-03 11:36+0200\n" 11 | "PO-Revision-Date: 2022-04-29 11:00+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: de_AT\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | msgid "id" 22 | msgstr "c" 23 | -------------------------------------------------------------------------------- /testdata/structure/de_AT.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the Poedit package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Project 1.0\n" 9 | "Report-Msgid-Bugs-To: help@example.com\n" 10 | "POT-Creation-Date: 2021-06-03 11:36+0200\n" 11 | "PO-Revision-Date: 2022-04-29 11:14+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: de_AT\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | msgid "id" 22 | msgstr "e" 23 | -------------------------------------------------------------------------------- /testdata/structure/base.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the Poedit package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Project 1.0\n" 10 | "Report-Msgid-Bugs-To: help@example.com\n" 11 | "POT-Creation-Date: 2021-06-03 11:36+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 20 | 21 | msgid "id" 22 | msgstr "" -------------------------------------------------------------------------------- /testdata/structure/de/my_category/b.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the Poedit package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Project 1.0\n" 9 | "Report-Msgid-Bugs-To: help@example.com\n" 10 | "POT-Creation-Date: 2021-06-03 11:36+0200\n" 11 | "PO-Revision-Date: 2022-04-29 10:58+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | msgid "id" 22 | msgstr "b" 23 | -------------------------------------------------------------------------------- /examples/features/httptempl/README.md: -------------------------------------------------------------------------------- 1 | ### Spreak + po + `http/template` 2 | 3 | The following example shows how to use Spreak with the translation managed in `po` files and `html/template` 4 | for a web project. 5 | For an example with json files, see the [jhttptempl](../jhttptempl) example. 6 | 7 | In the `locale/` folder you will find the files for the translations. 8 | The file `locale/httptempl.pot` can be used as a template for new translations. 9 | In `templates/` you will find the template files which are delivered by the web server. 10 | 11 | To start the web server just use `go run .`. 12 | 13 | To update the `httptempl.pot` the following command can be used: 14 | ```shell 15 | xspreak -D ./ -o locale/httptempl.pot -k ".i18n.Tr" -k "$.i18n.Tr" -t "templates/*.html" 16 | ``` -------------------------------------------------------------------------------- /internal/calendar/calendar_test.go: -------------------------------------------------------------------------------- 1 | package calendar 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_isLeapYear(t *testing.T) { 10 | for _, year := range []int{2004, 2008, 2012, 2016, 2020, 2024, 2028, 2032, 2036, 2040} { 11 | assert.True(t, IsLeap(year)) 12 | } 13 | 14 | for _, year := range []int{2005, 2006, 2007, 1985, 2034} { 15 | assert.False(t, IsLeap(year), year) 16 | } 17 | } 18 | 19 | func TestLeapDays(t *testing.T) { 20 | assert.Equal(t, 2, LeapDays(2016, 2022)) 21 | assert.Equal(t, -2, LeapDays(2022, 2016)) 22 | assert.Equal(t, 0, LeapDays(2001, 2003)) 23 | } 24 | 25 | func TestDaysInMonth(t *testing.T) { 26 | assert.Equal(t, 31, DaysInMonth(2022, 05)) 27 | assert.Equal(t, 29, DaysInMonth(2024, 02)) 28 | } 29 | -------------------------------------------------------------------------------- /catalog/cldrplural/ast/walk.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | type Visitor interface { 4 | Visit(node Node) (w Visitor) 5 | } 6 | 7 | func Walk(v Visitor, node Node) { 8 | if v = v.Visit(node); v == nil { 9 | return 10 | } 11 | 12 | switch n := node.(type) { 13 | case *RangeListExpr: 14 | Walk(v, n.X) 15 | if n.Y != nil { 16 | Walk(v, n.Y) 17 | } 18 | case *ModuloExpr: 19 | Walk(v, n.Op) 20 | case *BinaryExpr: 21 | Walk(v, n.X) 22 | Walk(v, n.Y) 23 | case *InRelationExpr: 24 | Walk(v, n.X) 25 | Walk(v, n.Y) 26 | } 27 | 28 | v.Visit(nil) 29 | } 30 | 31 | type inspector func(Node) bool 32 | 33 | func (f inspector) Visit(node Node) Visitor { 34 | if f(node) { 35 | return f 36 | } 37 | return nil 38 | } 39 | 40 | func Inspect(node Node, f func(Node) bool) { 41 | Walk(inspector(f), node) 42 | } 43 | -------------------------------------------------------------------------------- /examples/features/embed/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io/fs" 7 | 8 | "golang.org/x/text/language" 9 | 10 | "github.com/vorlif/spreak" 11 | ) 12 | 13 | const ( 14 | HelloWorldDomain = "helloworld" 15 | ) 16 | 17 | //go:embed locale/* 18 | var locales embed.FS 19 | 20 | func main() { 21 | fsys, _ := fs.Sub(locales, "locale") 22 | bundle, err := spreak.NewBundle( 23 | spreak.WithSourceLanguage(language.English), 24 | spreak.WithDefaultDomain(HelloWorldDomain), 25 | spreak.WithDomainFs(HelloWorldDomain, fsys), 26 | spreak.WithLanguage(language.German, language.Spanish, language.French), 27 | ) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | t := spreak.NewLocalizer(bundle, language.Spanish) 33 | 34 | fmt.Println(t.Get("Hello world")) 35 | // Output: Hola Mundo 36 | } 37 | -------------------------------------------------------------------------------- /catalog/poplural/ast/token.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | type Token string 4 | 5 | const ( 6 | eof Token = "eof" 7 | whitespace Token = "ws" 8 | failure Token = "failure" 9 | Value Token = "Value" // 1, 2, 100, etc. 10 | Operand Token = "n" 11 | plural Token = "plural=" 12 | nPlurals Token = "nplurals=" 13 | Equal Token = "==" 14 | assign Token = "=" 15 | Greater Token = ">" 16 | GreaterOrEqual Token = ">=" 17 | Less Token = "<" 18 | LessOrEqual Token = "<=" 19 | Reminder Token = "%" 20 | NotEqual Token = "!=" 21 | LogicalAnd Token = "&&" 22 | LogicalOr Token = "||" 23 | Question Token = "?" 24 | colon Token = ":" 25 | semicolon Token = ";" 26 | leftBracket Token = "(" 27 | rightBracket Token = ")" 28 | ) 29 | -------------------------------------------------------------------------------- /examples/locale/printer.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-05-08 21:56+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #. TRANSLATORS: %{name}s is the name of a person 21 | #: ../features/printer/main.go:34 22 | msgid "My name is %{name}s and I am %{age}d years old" 23 | msgstr "" 24 | 25 | -------------------------------------------------------------------------------- /catalog/cldrplural/builtin.go: -------------------------------------------------------------------------------- 1 | package cldrplural 2 | 3 | import "golang.org/x/text/language" 4 | 5 | // Language that is used for the rule if no rule can be found for a language. 6 | var fallbackLanguage = "en" 7 | 8 | // ForLanguage returns the set of rules for a language. 9 | // If no matching language is found, the English rule set and false are returned. 10 | func ForLanguage(lang language.Tag) (*RuleSet, bool) { 11 | n := lang 12 | for !n.IsRoot() { 13 | if ruleSet := getBuiltInForLanguage(n.String()); ruleSet != nil { 14 | return ruleSet, true 15 | } 16 | 17 | base, confidence := n.Base() 18 | if confidence >= language.High { 19 | if ruleSet := getBuiltInForLanguage(base.String()); ruleSet != nil { 20 | return ruleSet, true 21 | } 22 | } 23 | 24 | n = n.Parent() 25 | } 26 | 27 | return getBuiltInForLanguage(fallbackLanguage), false 28 | } 29 | -------------------------------------------------------------------------------- /examples/features/jhttptempl/locale/httptempl.json: -------------------------------------------------------------------------------- 1 | { 2 | "forms.address": "", 3 | "forms.cart": "", 4 | "forms.checkout_title": "", 5 | "forms.continue": "", 6 | "forms.description": "", 7 | "forms.first": "", 8 | "forms.header": "", 9 | "forms.keep_infos": "", 10 | "forms.lead": "", 11 | "forms.name": "", 12 | "forms.password": "", 13 | "forms.product": "", 14 | "forms.total": "", 15 | "home.choose_language": "", 16 | "home.forms": "", 17 | "home.header": "", 18 | "home.lead": "", 19 | "home.selection_field": "", 20 | "home.title": "", 21 | "not_found.great_user": "", 22 | "not_found.next_steps": "", 23 | "not_found.not_exist": "", 24 | "not_found.stupid_statement": { 25 | "one": "", 26 | "other": "" 27 | }, 28 | "not_found.welcome": "", 29 | "not_found.wish": { 30 | "one": "", 31 | "other": "" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /catalog/json_encoder.go: -------------------------------------------------------------------------------- 1 | package catalog 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | ) 7 | 8 | // JSONEncoder is an encoder for writing JSON catalog files. 9 | type JSONEncoder struct { 10 | w io.Writer 11 | } 12 | 13 | // NewJSONEncoder returns a new encoder that writes a JSON catalog to w. 14 | func NewJSONEncoder(w io.Writer) *JSONEncoder { return &JSONEncoder{w: w} } 15 | 16 | // Encode converts a JSON messages map into the content of a JSON catalog file. 17 | // 18 | // If nil or an empty map is passed, an empty JSON object is written. 19 | func (enc JSONEncoder) Encode(cat JSONCatalog) error { 20 | if cat == nil { 21 | _, err := enc.w.Write([]byte("{}")) 22 | return err 23 | } 24 | 25 | jsonEnc := json.NewEncoder(enc.w) 26 | jsonEnc.SetIndent("", " ") 27 | if err := jsonEnc.Encode(cat); err != nil { 28 | return err 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /humanize/locale/locale_test.go: -------------------------------------------------------------------------------- 1 | package locale 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/vorlif/spreak/catalog/po" 13 | ) 14 | 15 | func TestParsePo(t *testing.T) { 16 | 17 | errW := filepath.WalkDir("./", func(path string, d fs.DirEntry, err error) error { 18 | assert.NoError(t, err) 19 | if filepath.Ext(path) != ".po" { 20 | return nil 21 | } 22 | 23 | data, errR := os.ReadFile(path) 24 | if assert.NoError(t, errR) { 25 | assert.NotNil(t, data) 26 | assert.NotEmpty(t, data) 27 | 28 | poFile, errP := po.Parse(data) 29 | assert.NoError(t, errP) 30 | assert.NotNil(t, poFile) 31 | assert.NotEmpty(t, poFile.Messages) 32 | } 33 | 34 | return nil 35 | }) 36 | 37 | require.NoError(t, errW) 38 | 39 | } 40 | -------------------------------------------------------------------------------- /examples/locale/de/helloworld.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-06 14:53+0200\n" 11 | "PO-Revision-Date: 2022-05-06 14:56+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | #. TRANSLATORS: This comment is automatically extracted by xspreak 22 | #. and can be used to leave useful hints for the translators. 23 | #: ../helloworld/main.go:26 24 | msgid "Hello world" 25 | msgstr "Hallo Welt" 26 | -------------------------------------------------------------------------------- /testdata/translation-test/json/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.name": "TODO List", 3 | "date.weekday": "Weekday", 4 | "date.monday": "Monday", 5 | "date.friday": "Friday", 6 | "user.goodbye": "Goodbye %s", 7 | "animals": { 8 | "one": "I have %v animal", 9 | "other": "I have %v animals" 10 | }, 11 | "animal.cat": { 12 | "one": "I do not have a cat", 13 | "other": "I do not have cats" 14 | }, 15 | "animal.dog_my-animals": { 16 | "context": "my-animals", 17 | "one": "I have a dog", 18 | "other": "I have dogs" 19 | }, 20 | "animal_world": { 21 | "context": "world", 22 | "one": "There is one animal in the world", 23 | "other": "There are %d animals in the world" 24 | }, 25 | "animal.dog": { 26 | "one": "Someone has a dog", 27 | "other": "Someone has dogs" 28 | }, 29 | "animal.name_my-animals": { 30 | "context": "my-animals", 31 | "other": "My pet's name is %s" 32 | } 33 | } -------------------------------------------------------------------------------- /examples/features/embed/locale/de.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-06 14:53+0200\n" 11 | "PO-Revision-Date: 2022-05-06 14:56+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | #. TRANSLATORS: This comment is automatically extracted by xspreak 22 | #. and can be used to leave useful hints for the translators. 23 | #: ../helloworld/main.go:26 24 | msgid "Hello world" 25 | msgstr "Hallo Welt" 26 | -------------------------------------------------------------------------------- /examples/features/embed/locale/es.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-06 14:53+0200\n" 11 | "PO-Revision-Date: 2022-05-06 14:57+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: es\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | #. TRANSLATORS: This comment is automatically extracted by xspreak 22 | #. and can be used to leave useful hints for the translators. 23 | #: ../helloworld/main.go:26 24 | msgid "Hello world" 25 | msgstr "Hola Mundo" 26 | -------------------------------------------------------------------------------- /examples/features/embed/locale/fr.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-06 14:53+0200\n" 11 | "PO-Revision-Date: 2022-05-06 14:57+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | #. TRANSLATORS: This comment is automatically extracted by xspreak 22 | #. and can be used to leave useful hints for the translators. 23 | #: ../helloworld/main.go:26 24 | msgid "Hello world" 25 | msgstr "Bonjour à tous" 26 | -------------------------------------------------------------------------------- /examples/locale/fr/helloworld.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-06 14:53+0200\n" 11 | "PO-Revision-Date: 2022-05-06 14:57+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | #. TRANSLATORS: This comment is automatically extracted by xspreak 22 | #. and can be used to leave useful hints for the translators. 23 | #: ../helloworld/main.go:26 24 | msgid "Hello world" 25 | msgstr "Bonjour à tous" 26 | -------------------------------------------------------------------------------- /testdata/structure/es/helloworld.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-06 14:53+0200\n" 11 | "PO-Revision-Date: 2022-05-06 14:57+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: es\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | #. TRANSLATORS: This comment is automatically extracted by xspreak 22 | #. and can be used to leave useful hints for the translators. 23 | #: ../helloworld/main.go:26 24 | msgid "Hello world" 25 | msgstr "Hola Mundo" 26 | -------------------------------------------------------------------------------- /internal/calendar/calendar.go: -------------------------------------------------------------------------------- 1 | package calendar 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // IsLeap return true for leap years, false for non-leap years. 9 | // 10 | // Adapted from https://github.com/python/cpython/blob/main/Lib/calendar.py 11 | func IsLeap(year int) bool { 12 | return year%4 == 0 && (year%100 != 0 || year%400 == 0) 13 | } 14 | 15 | // LeapDays return number of leap years in range [y1, y2). Assume y1 <= y2. 16 | // 17 | // Adapted from https://github.com/python/cpython/blob/main/Lib/calendar.py 18 | func LeapDays(year1, year2 int) int { 19 | y1 := float64(year1 - 1) 20 | y2 := float64(year2 - 1) 21 | tmp := math.Floor(y2/4) - math.Floor(y1/4) 22 | tmp -= math.Floor(y2/100) - math.Floor(y1/100) 23 | tmp += math.Floor(y2/400) - math.Floor(y1/400) 24 | return int(tmp) 25 | } 26 | 27 | func DaysInMonth(year int, month time.Month) int { 28 | return time.Date(year, month, 1, 0, 0, 0, 0, time.UTC).AddDate(0, 1, -1).Day() 29 | } 30 | -------------------------------------------------------------------------------- /catalog/poplural/ast/compile_test.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCompileToString(t *testing.T) { 11 | tests := []struct { 12 | input string 13 | want string 14 | }{ 15 | {"nplurals=2; plural=n != 1;", "nplurals=2; plural=n != 1;"}, 16 | {"nplurals=2; plural= (n == 0) ? 0 : 1;", "nplurals=2; plural=(n == 0) ? 0 : 1;"}, 17 | {"nplurals=2; plural=n< 5;", "nplurals=2; plural=n < 5;"}, 18 | {"nplurals=2; plural=n >5;", "nplurals=2; plural=n > 5;"}, 19 | {"nplurals=2; plural=n >= 5;", "nplurals=2; plural=n >= 5;"}, 20 | {"nplurals=2; plural= n >= 5 && 5 < 3 || n != 2;", "nplurals=2; plural=(n >= 5 && 5 < 3 || n != 2);"}, 21 | } 22 | 23 | for _, tt := range tests { 24 | t.Run(fmt.Sprintf("%s => %s", tt.input, tt.want), func(t *testing.T) { 25 | parsed := MustParse(tt.input) 26 | assert.Equal(t, tt.want, CompileToString(parsed)) 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/locale/errors.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-05-06 14:53+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #. TRANSLATORS: It is the name of a file on the local computer. 21 | #: ../features/errors/main.go:19 22 | msgctxt "errors" 23 | msgid "name has an invalid format" 24 | msgstr "" 25 | 26 | #: ../features/errors/main.go:41 27 | msgid "another mistake has happened: %s" 28 | msgstr "" 29 | 30 | -------------------------------------------------------------------------------- /examples/locale/de/printer.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-06 17:53+0200\n" 11 | "PO-Revision-Date: 2022-05-06 17:54+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | #. TRANSLATORS: %{name}s is the name of a person 22 | #: ../features/printer/main.go:34 23 | msgid "My name is %{name}s and I am %{age}d years old" 24 | msgstr "Mein Name ist %{name}s und ich bin %{age}d Jahre alt" 25 | 26 | #~ msgid "My name is %{name}s" 27 | #~ msgstr "Mein Name ist %{name}s" 28 | -------------------------------------------------------------------------------- /catalog/cldrplural/evaluation_test.go: -------------------------------------------------------------------------------- 1 | // This file is generated by cldrplural/generator/generate.sh; DO NOT EDIT 2 | package cldrplural 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMustParseRules(t *testing.T) { 11 | t.Run("panic on invalid rule", func(t *testing.T) { 12 | rawRules := map[Category]string{ 13 | One: "abcdez", 14 | } 15 | f := func() { 16 | MustParseRules(rawRules) 17 | } 18 | 19 | assert.Panics(t, f) 20 | }) 21 | } 22 | 23 | func TestParseRules(t *testing.T) { 24 | rawRules := map[Category]string{One: "i = 1 and v = 0"} 25 | set := MustParseRules(rawRules) 26 | assert.NotNil(t, set) 27 | assert.NotNil(t, set.Categories) 28 | assert.NotNil(t, set.FormFunc) 29 | assert.ElementsMatch(t, []Category{One, Other}, set.Categories) 30 | assert.Equal(t, One, set.FormFunc(MustNewOperands("1"))) 31 | assert.Equal(t, Other, set.FormFunc(MustNewOperands("1.0"))) 32 | assert.Equal(t, Other, set.FormFunc(MustNewOperands("1.1"))) 33 | } 34 | -------------------------------------------------------------------------------- /testdata/structure/de/LC_MESSAGES/z.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the Poedit package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Project 1.0\n" 9 | "Report-Msgid-Bugs-To: help@example.com\n" 10 | "POT-Creation-Date: 2021-06-03 11:36+0200\n" 11 | "PO-Revision-Date: 2022-04-29 10:58+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | msgid "id" 22 | msgstr "a" 23 | 24 | msgid "Malformed header: “%s”" 25 | msgstr "" 26 | 27 | msgid "variable %s test" 28 | msgstr "Test of interpolation of variables %s" 29 | 30 | msgid "%d issue" 31 | msgid_plural "%d issues" 32 | msgstr[0] "%d Problem" 33 | msgstr[1] "%d Probleme" -------------------------------------------------------------------------------- /catalog/po/message.go: -------------------------------------------------------------------------------- 1 | package po 2 | 3 | // Message is a representation of a single message in a catalog. 4 | type Message struct { 5 | Comment *Comment 6 | Context string 7 | ID string 8 | IDPlural string 9 | Str map[int]string 10 | } 11 | 12 | func NewMessage() *Message { 13 | return &Message{ 14 | Comment: &Comment{ 15 | References: make([]*Reference, 0), 16 | Flags: make([]string, 0), 17 | }, 18 | Context: "", 19 | ID: "", 20 | IDPlural: "", 21 | Str: make(map[int]string, 1), 22 | } 23 | } 24 | 25 | func (m *Message) AddReference(ref *Reference) { 26 | if m.Comment == nil { 27 | m.Comment = NewComment() 28 | } 29 | 30 | m.Comment.AddReference(ref) 31 | } 32 | 33 | func (m *Message) Merge(other *Message) { 34 | if other == nil { 35 | return 36 | } 37 | 38 | if m.Comment == nil { 39 | m.Comment = NewComment() 40 | } 41 | m.Comment.Merge(other.Comment) 42 | 43 | if m.IDPlural == "" && other.IDPlural != "" { 44 | m.IDPlural = other.IDPlural 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/locale/de/errors.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-06 14:53+0200\n" 11 | "PO-Revision-Date: 2022-05-06 15:14+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | #. TRANSLATORS: It is the name of a file on the local computer. 22 | #: ../features/errors/main.go:19 23 | msgctxt "errors" 24 | msgid "name has an invalid format" 25 | msgstr "der Name hat ein ungültiges Format" 26 | 27 | #: ../features/errors/main.go:41 28 | msgid "another mistake has happened: %s" 29 | msgstr "ein weiterer Fehler ist passiert: %s" 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 6 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 7 | golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 8 | golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /examples/features/jhttptempl/locale/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "forms.address": "Rechnungsadresse", 3 | "forms.cart": "", 4 | "forms.checkout_title": "", 5 | "forms.continue": "", 6 | "forms.description": "", 7 | "forms.first": "Vorname", 8 | "forms.header": "", 9 | "forms.keep_infos": "", 10 | "forms.lead": "", 11 | "forms.name": "", 12 | "forms.password": "", 13 | "forms.product": "", 14 | "forms.total": "", 15 | "home.choose_language": "Wähle eine Sprache", 16 | "home.forms": "Formular", 17 | "home.header": "", 18 | "home.lead": "Spreak ist eine flexible Übersetzungs- und Humanisierungsbibliothek für Go, basierend auf den Konzepten von gettext", 19 | "home.selection_field": "", 20 | "home.title": "", 21 | "not_found.great_user": "", 22 | "not_found.next_steps": "", 23 | "not_found.not_exist": "", 24 | "not_found.stupid_statement": { 25 | "one": "", 26 | "other": "" 27 | }, 28 | "not_found.welcome": "", 29 | "not_found.wish": { 30 | "one": "Wir wünschen dir einen schönen Tag", 31 | "other": "Wir wünschen dir schöne Tage" 32 | } 33 | } -------------------------------------------------------------------------------- /examples/locale/helloworld.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-05-16 09:10+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #. TRANSLATORS: This comment is automatically extracted by xspreak 21 | #. and can be used to leave useful hints for the translators. 22 | #: ../helloworld/main.go:28 ../helloworld/main.go:31 23 | msgid "Hello world" 24 | msgstr "" 25 | 26 | #: ../helloworld/main.go:41 27 | msgid "I do not know any planet" 28 | msgid_plural "I do not know any planets" 29 | msgstr[0] "" 30 | msgstr[1] "" 31 | 32 | -------------------------------------------------------------------------------- /testdata/translation-test/de/LC_MESSAGES/z.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the Poedit package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Project 1.0\n" 9 | "Report-Msgid-Bugs-To: help@example.com\n" 10 | "POT-Creation-Date: 2021-06-03 11:36+0200\n" 11 | "PO-Revision-Date: 2022-04-29 10:58+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | msgid "id" 22 | msgstr "a" 23 | 24 | msgid "Malformed header: “%s”" 25 | msgstr "" 26 | 27 | msgid "variable %s test" 28 | msgstr "Test of interpolation of variables %s" 29 | 30 | msgid "%d issue" 31 | msgid_plural "%d issues" 32 | msgstr[0] "%d Problem" 33 | msgstr[1] "%d Probleme" 34 | 35 | msgid "The kindergarten" 36 | msgstr "Der Kindergarten" -------------------------------------------------------------------------------- /catalog/po/scanner_test.go: -------------------------------------------------------------------------------- 1 | package po 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestScanner_ScanComment(t *testing.T) { 11 | t.Run("header comment", func(t *testing.T) { 12 | comment := `# This file is distributed under the same license as the Django package. 13 | # 14 | # Translators: 15 | # F Wolff , 2019-2020 16 | # Stephen Cox , 2011-2012 17 | # unklphil , 2014,2019` 18 | 19 | s := newScanner(bytes.NewReader([]byte(comment))) 20 | assert.NotNil(t, s) 21 | 22 | tests := []string{ 23 | "# This file is distributed under the same license as the Django package.", 24 | "#", 25 | "# Translators:", 26 | "# F Wolff , 2019-2020", 27 | "# Stephen Cox , 2011-2012", 28 | "# unklphil , 2014,2019", 29 | } 30 | 31 | for _, tt := range tests { 32 | tok, lit := s.scan() 33 | assert.Equal(t, commentTranslator, tok) 34 | assert.Equal(t, tt, lit) 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - forbidigo 5 | - godot 6 | - misspell 7 | - revive 8 | - unconvert 9 | settings: 10 | cyclop: 11 | max-complexity: 15 12 | package-average: 0 13 | forbidigo: 14 | forbid: 15 | - pattern: fmt\.Print.* 16 | godot: 17 | scope: declarations 18 | exclude: 19 | - '^fixme:' 20 | - '^todo:' 21 | capital: false 22 | period: true 23 | revive: 24 | rules: 25 | - name: unused-parameter 26 | disabled: true 27 | exclusions: 28 | generated: lax 29 | presets: 30 | - comments 31 | - common-false-positives 32 | - legacy 33 | - std-error-handling 34 | paths: 35 | - third_party$ 36 | - builtin$ 37 | - examples$ 38 | issues: 39 | fix: true 40 | formatters: 41 | enable: 42 | - gofmt 43 | - goimports 44 | settings: 45 | goimports: 46 | local-prefixes: 47 | - github.com/vorlif/spreak 48 | exclusions: 49 | generated: lax 50 | paths: 51 | - third_party$ 52 | - builtin$ 53 | - examples$ 54 | -------------------------------------------------------------------------------- /humanize/locale.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "io/fs" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak" 9 | "github.com/vorlif/spreak/catalog" 10 | ) 11 | 12 | type localeLoader struct { 13 | locales map[language.Tag]*LocaleData 14 | } 15 | 16 | func newLoader(locales []*LocaleData) *localeLoader { 17 | loader := &localeLoader{ 18 | locales: make(map[language.Tag]*LocaleData, len(locales)), 19 | } 20 | for _, loc := range locales { 21 | loader.locales[loc.Lang] = loc 22 | } 23 | return loader 24 | } 25 | 26 | func (loader *localeLoader) Load(lang language.Tag, domain string) (catalog.Catalog, error) { 27 | data, hasData := loader.locales[lang] 28 | if !hasData { 29 | return nil, spreak.NewErrNotFound(lang, "humanize", "domain=%q", domain) 30 | } 31 | 32 | content, err := fs.ReadFile(data.Fs, domain+".po") 33 | if err != nil { 34 | return nil, spreak.NewErrNotFound(lang, "humanize", "domain=%q,err=%v", domain, err) 35 | } 36 | 37 | dec := catalog.NewPoDecoder() 38 | cat, err := dec.Decode(lang, domain, content) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return cat, nil 44 | } 45 | -------------------------------------------------------------------------------- /examples/locale/es/helloworld.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-16 09:10+0200\n" 11 | "PO-Revision-Date: 2022-05-16 09:11+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: es\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | #. TRANSLATORS: This comment is automatically extracted by xspreak 22 | #. and can be used to leave useful hints for the translators. 23 | #: ../helloworld/main.go:28 ../helloworld/main.go:31 24 | msgid "Hello world" 25 | msgstr "Hola Mundo" 26 | 27 | #: ../helloworld/main.go:41 28 | msgid "I do not know any planet" 29 | msgid_plural "I do not know any planets" 30 | msgstr[0] "No conozco ningún planeta" 31 | msgstr[1] "No conozco ningún planeta" 32 | -------------------------------------------------------------------------------- /catalog/json_decoder_test.go: -------------------------------------------------------------------------------- 1 | package catalog 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "golang.org/x/text/language" 10 | ) 11 | 12 | func TestJsonDecoder_Decode(t *testing.T) { 13 | t.Run("returns error on invalid json", func(t *testing.T) { 14 | cat, errC := NewJSONDecoder().Decode(language.English, "a", []byte("invalid json")) 15 | assert.Error(t, errC) 16 | require.Nil(t, cat) 17 | }) 18 | 19 | t.Run("returns error on empty file", func(t *testing.T) { 20 | cat, errC := NewJSONDecoder().Decode(language.English, "a", []byte("")) 21 | assert.Error(t, errC) 22 | require.Nil(t, cat) 23 | 24 | cat, errC = NewJSONDecoder().Decode(language.English, "a", []byte("{}")) 25 | assert.Error(t, errC) 26 | require.Nil(t, cat) 27 | }) 28 | 29 | t.Run("returns no error on valid input", func(t *testing.T) { 30 | data, err := os.ReadFile(enJSONTestFile) 31 | assert.NoError(t, err) 32 | require.NotNil(t, data) 33 | 34 | cat, errC := NewJSONDecoder().Decode(language.English, "a", data) 35 | assert.NoError(t, errC) 36 | require.NotNil(t, cat) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /catalog/poplural/generator/extra.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func getExtraRules() []*ruleData { 4 | list := make([]*ruleData, len(extraRulesTable)) 5 | for i, rawRule := range extraRulesTable { 6 | list[i] = newRuleData(rawRule) 7 | } 8 | return list 9 | } 10 | 11 | // Separate list of rules that are not in the JSON file but are frequently used. 12 | var extraRulesTable = []string{ 13 | "nplurals=3; plural=n == 0 ? 0 : (n == 0 || n == 1) && n != 0 ? 1 : 2;", 14 | "nplurals=4; plural=n % 100 == 1 ? 0 : n % 100 == 2 ? 1 : n % 100 >= 3 && n % 100 <= 4 ? 2 : 3;", 15 | "nplurals=2; plural=n % 10 == 1 && n % 100 != 11;", 16 | "nplurals=2; plural=((n == 1 || (n == 2 || n == 3)) || n % 10 != 4 && n % 10 != 6 && n % 10 != 9);", 17 | "nplurals=2; plural=(n == 0 || n == 1);", 18 | "nplurals=5; plural=n % 10 == 1 && n % 100 != 11 && n % 100 != 71 && n % 100 != 91 ? 0 : n % 10 == 2 && n % 100 != 12 && n % 100 != 72 && n % 100 != 92 ? 1 : (n % 10 >= 3 && n % 10 <= 4 || n % 10 == 9) && (n % 100 < 10 || n % 100 > 19) && (n % 100 < 70 || n % 100 > 79) && (n % 100 < 90 || n % 100 > 99) ? 2 : n != 0 && n % 1000000 == 0 ? 3 : 4;", 19 | "nplurals=2; plural=(n <= 1 || n >= 11 && n <= 99);", 20 | } 21 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Xuanwo/go-locale v1.1.3 h1:EWZZJJt5rqPHHbqPRH1zFCn5D7xHjjebODctA4aUO3A= 2 | github.com/Xuanwo/go-locale v1.1.3/go.mod h1:REn+F/c+AtGSWYACBSYZgl23AP+0lfQC+SEFPN+hj30= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 8 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 9 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 10 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 11 | golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= 12 | golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /examples/helloworld/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak" 9 | "github.com/vorlif/spreak/localize" 10 | ) 11 | 12 | func main() { 13 | bundle, err := spreak.NewBundle( 14 | spreak.WithSourceLanguage(language.English), 15 | spreak.WithDefaultDomain("helloworld"), 16 | spreak.WithDomainPath("helloworld", "../locale"), 17 | spreak.WithRequiredLanguage(language.Spanish), 18 | spreak.WithLanguage(language.German, language.French), 19 | ) 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | t := spreak.NewLocalizer(bundle, language.Spanish) 25 | 26 | // TRANSLATORS: This comment is automatically extracted by xspreak 27 | // and can be used to leave useful hints for the translators. 28 | fmt.Println(t.Get("Hello world")) 29 | fmt.Println(t.Localize(GetPlanet())) 30 | 31 | t = spreak.NewLocalizer(bundle, language.English) 32 | fmt.Println(t.Get("Hello world")) 33 | 34 | // Output: 35 | // Hola Mundo 36 | // No conozco ningún planeta 37 | // Hello world 38 | } 39 | 40 | func GetPlanet() *localize.Message { 41 | return &localize.Message{ 42 | Singular: "I do not know any planet", 43 | Plural: "I do not know any planets", 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /catalog/cldrplural/ast/ast.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | type Rule struct { 4 | Root Node 5 | Samples []string 6 | } 7 | 8 | type Node interface { 9 | Type() Token 10 | } 11 | 12 | type OperandExpr struct { 13 | Operand string 14 | } 15 | 16 | type ValueExpr struct { 17 | Value int64 18 | } 19 | 20 | type RangeExpr struct { 21 | From int64 22 | To int64 23 | } 24 | 25 | type RangeListExpr struct { 26 | X Node // (range | value) 27 | Y Node // (',' range_list)* Optional! 28 | } 29 | 30 | type ModuloExpr struct { 31 | Op *OperandExpr 32 | Value int64 33 | } 34 | 35 | type BinaryExpr struct { 36 | X Node 37 | Op Token // and, or 38 | Y Node 39 | } 40 | 41 | type InRelationExpr struct { 42 | X Node // operand (('mod' | '%') value)? 43 | Op Token // ==, != 44 | Y *RangeListExpr // 45 | } 46 | 47 | func (OperandExpr) Type() Token { return Operand } 48 | func (ValueExpr) Type() Token { return Value } 49 | func (RangeExpr) Type() Token { return ValueRange } 50 | func (RangeListExpr) Type() Token { return RangeList } 51 | func (ModuloExpr) Type() Token { return Remainder } 52 | func (e BinaryExpr) Type() Token { return e.Op } 53 | func (e InRelationExpr) Type() Token { return e.Op } 54 | -------------------------------------------------------------------------------- /catalog/poplural/builtin.go: -------------------------------------------------------------------------------- 1 | package poplural 2 | 3 | import ( 4 | "golang.org/x/text/language" 5 | ) 6 | 7 | // Language that is used for the rule if no rule can be found for a language. 8 | var fallbackLanguage = "en" 9 | 10 | // PluralFunc is a function that returns the appropriate plural form for a value. 11 | type PluralFunc = func(a any) (int, error) 12 | 13 | // ForLanguage searches for the appropriate built-in plural function for a language. 14 | // If no function is found, the English plural function is automatically used. 15 | // The second return value indicates whether a suitable function was found or whether the fallback is used. 16 | func ForLanguage(lang language.Tag) (PluralFunc, bool) { 17 | form, found := pluralRuleForLanguage(lang) 18 | return form.Evaluate, found 19 | } 20 | 21 | func pluralRuleForLanguage(lang language.Tag) (*Rule, bool) { 22 | n := lang 23 | for !n.IsRoot() { 24 | if rule := getBuiltInForLanguage(n.String()); rule != nil { 25 | return rule, true 26 | } 27 | 28 | base, confidence := n.Base() 29 | if confidence >= language.High { 30 | if rule := getBuiltInForLanguage(base.String()); rule != nil { 31 | return rule, true 32 | } 33 | } 34 | 35 | n = n.Parent() 36 | } 37 | 38 | return getBuiltInForLanguage(fallbackLanguage), false 39 | } 40 | -------------------------------------------------------------------------------- /catalog/po/token.go: -------------------------------------------------------------------------------- 1 | package po 2 | 3 | type token int 4 | 5 | const ( 6 | eof token = iota // "eof" 7 | whitespace // "ws" 8 | failure // "failure" 9 | commentTranslator // "#" 10 | commentExtracted // "#." 11 | commentReference // "#:" 12 | commentFlags // "#," 13 | commentPrevContext // "#| msgctxt" 14 | commentPrevContextLine // "#| \"prev-ctx\"" 15 | commentPrevMsgID // "#| msgid" 16 | commentPrevMsgIDLine // "#| \"prev-msg\"" 17 | commentPrevUnknown // "#| \"unknown\"" 18 | msgContext // "msgctxt" 19 | msgContextLine // "\"more context\"" 20 | msgID // "msgid" 21 | msgIDLine // "\"singular\"" 22 | msgIDPlural // "msgid_plural" 23 | msgIDPluralLine // "\"plural\"" 24 | msgStr // "msgstr" 25 | msgStrLine // "\"singular translation\"" 26 | msgStrPlural // "msgstr[?]" 27 | msgStrPluralLine // "\"plural translation\"" 28 | none // internal state 29 | ) 30 | -------------------------------------------------------------------------------- /humanize/locale_test.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "golang.org/x/text/language" 10 | ) 11 | 12 | func TestLoader(t *testing.T) { 13 | t.Run("no languages registered", func(t *testing.T) { 14 | l := newLoader([]*LocaleData{}) 15 | assert.Empty(t, l.locales) 16 | 17 | cat, err := l.Load(language.German, djangoDomain) 18 | assert.Error(t, err) 19 | assert.Nil(t, cat) 20 | }) 21 | 22 | t.Run("languages registered", func(t *testing.T) { 23 | l := newLoader([]*LocaleData{testGermanLocaleData}) 24 | assert.Len(t, l.locales, 1) 25 | 26 | cat, err := l.Load(language.German, djangoDomain) 27 | assert.NoError(t, err) 28 | assert.NotNil(t, cat) 29 | }) 30 | 31 | t.Run("invalid filesystem", func(t *testing.T) { 32 | path := filepath.Join(testdataDir, "en") 33 | 34 | l := newLoader([]*LocaleData{{Lang: language.German, Fs: os.DirFS(path), Format: nil}}) 35 | cat, err := l.Load(language.German, djangoDomain) 36 | assert.Error(t, err) 37 | assert.Nil(t, cat) 38 | }) 39 | 40 | t.Run("invalid file", func(t *testing.T) { 41 | path := filepath.Join(testdataDir, "es") 42 | l := newLoader([]*LocaleData{{Lang: language.Spanish, Fs: os.DirFS(path), Format: nil}}) 43 | cat, err := l.Load(language.Spanish, djangoDomain) 44 | assert.Error(t, err) 45 | assert.Nil(t, cat) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /examples/features/jhttptempl/README.md: -------------------------------------------------------------------------------- 1 | ### Spreak + json + `http/template` 2 | 3 | The following example shows how to use Spreak with the translation managed in key-value JSON files and `html/template` 4 | for a web project. 5 | For an example with po files, see the [httptempl](../httptempl) example. 6 | 7 | In the `locale/` folder you will find the files for the translations. 8 | The file `locale/httptempl.json` can be used as a template for new translations. 9 | In `templates/` you will find the template files which are delivered by the web server. 10 | 11 | To start the web server just use `go run .`. 12 | 13 | 1. To update the `httptempl.json` the following command can be used: 14 | ```shell 15 | xspreak -D ./ -o locale/httptempl.json -k ".i18n.Tr" -k "$.i18n.Tr" -k ".i18n.TrN:1,1" -t "templates/*.html" -f json --template-use-kv 16 | ``` 17 | 18 | 2. To create a translation file for a new language use 19 | ```shell 20 | xspreak merge -i locale/httptempl.json -o locale/de.json -l de 21 | # alternative with copy of english translations 22 | xspreak merge -i locale/en.json -o locale/de.json -l de 23 | ``` 24 | 25 | 3. To add new keys for an existing language use: 26 | ```shell 27 | xspreak merge -i locale/httptempl.json -o locale/de.json -l de 28 | # alternative with copy of english translations 29 | xspreak merge -i locale/en.json -o locale/de.json -l de 30 | ``` -------------------------------------------------------------------------------- /catalog/json_encoder_test.go: -------------------------------------------------------------------------------- 1 | package catalog 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "golang.org/x/text/language" 9 | 10 | "github.com/vorlif/spreak/catalog/cldrplural" 11 | ) 12 | 13 | func TestJsonEncoder_Encode(t *testing.T) { 14 | t.Run("return an empty object on invalid input", func(t *testing.T) { 15 | buf := &bytes.Buffer{} 16 | enc := NewJSONEncoder(buf) 17 | 18 | for _, input := range []JSONCatalog{nil, NewJSONCatalog(language.English, "")} { 19 | buf.Reset() 20 | err := enc.Encode(input) 21 | assert.NoError(t, err) 22 | assert.JSONEq(t, "{}", buf.String()) 23 | } 24 | }) 25 | 26 | t.Run("msgId is reused", func(t *testing.T) { 27 | cat := NewJSONCatalog(language.English, "").(*jsonCatalog) 28 | cat.mustSetMessage("car_ctx", &JSONMessage{ 29 | Context: "ctx", 30 | Translations: map[cldrplural.Category]string{ 31 | cldrplural.One: "Car", 32 | cldrplural.Other: "Cars", 33 | }, 34 | }) 35 | cat.mustSetMessage("car", &JSONMessage{ 36 | Translations: map[cldrplural.Category]string{ 37 | cldrplural.One: "Car", 38 | cldrplural.Other: "Cars", 39 | }, 40 | }) 41 | 42 | res := ` 43 | { 44 | "car": { 45 | "one": "Car", 46 | "other": "Cars" 47 | }, 48 | "car_ctx": { 49 | "context": "ctx", 50 | "one": "Car", 51 | "other": "Cars" 52 | } 53 | } 54 | ` 55 | 56 | buf := &bytes.Buffer{} 57 | err := NewJSONEncoder(buf).Encode(cat) 58 | assert.NoError(t, err) 59 | assert.JSONEq(t, res, buf.String()) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /examples/features/errors/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "golang.org/x/text/language" 8 | 9 | "github.com/vorlif/spreak" 10 | ) 11 | 12 | // When xspreak is run with the -e option, the strings for errors.New are also extracted. 13 | // They can then be easily translated. 14 | var ( 15 | // TRANSLATORS: It is the name of a file on the local computer. 16 | // 17 | // This comment is not extracted because a blank line was inserted above it. 18 | ErrInvalidName = errors.New("name has an invalid format") 19 | ) 20 | 21 | func main() { 22 | bundle, err := spreak.NewBundle( 23 | spreak.WithSourceLanguage(language.English), 24 | spreak.WithDefaultDomain("errors"), 25 | spreak.WithDomainPath("errors", "../../locale"), 26 | spreak.WithLanguage(language.German), 27 | ) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | t := spreak.NewLocalizer(bundle, language.German) 33 | fmt.Println(t.LocalizeError(ErrInvalidName)) 34 | 35 | // The following error is not automatically extracted 36 | // because it is not known what the entire string is until the program runs. 37 | untranslatable := fmt.Errorf("a mistake has happened: %w", ErrInvalidName) 38 | fmt.Println(untranslatable) 39 | 40 | translatable := fmt.Errorf(t.Get("another mistake has happened: %s"), t.LocalizeError(ErrInvalidName)) 41 | fmt.Println(translatable) 42 | 43 | // Output: 44 | // der Name hat ein ungültiges Format 45 | // a mistake has happened: name has an invalid format 46 | // ein weiterer Fehler ist passiert: der Name hat ein ungültiges Format 47 | } 48 | -------------------------------------------------------------------------------- /examples/features/loaders/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak" 9 | "github.com/vorlif/spreak/catalog" 10 | ) 11 | 12 | const ( 13 | Domain = "mydomain" 14 | ) 15 | 16 | // A loader allows you to load data from any source. 17 | // For example, you can use it to load the translation from the database, a MinIO instance, 18 | // or from your Facebook page. 19 | type myLoader struct{} 20 | 21 | var _ spreak.Loader = (*myLoader)(nil) 22 | 23 | func (myLoader) Load(lang language.Tag, domain string) (catalog.Catalog, error) { 24 | if lang == language.Spanish && domain == Domain { 25 | decoder := catalog.NewPoDecoder() 26 | return decoder.Decode(lang, domain, esTranslations) 27 | } 28 | 29 | return nil, spreak.NewErrNotFound(lang, "code", "domain=%q", domain) 30 | } 31 | 32 | // An example of loaded data. Here we use the format of the Po files since we have a decoder. 33 | // You can use any format you like as long as you can create a catalog for it. 34 | var esTranslations = []byte(` 35 | msgid "Hello world" 36 | msgstr "Hola Mundo" 37 | `) 38 | 39 | func main() { 40 | bundle, err := spreak.NewBundle( 41 | spreak.WithSourceLanguage(language.English), 42 | spreak.WithDefaultDomain(Domain), 43 | // Use the loader 44 | spreak.WithDomainLoader(Domain, &myLoader{}), 45 | spreak.WithLanguage(language.Spanish), 46 | ) 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | t := spreak.NewLocalizer(bundle, language.Spanish) 52 | fmt.Println(t.Get("Hello world")) 53 | // Output: Hola Mundo 54 | } 55 | -------------------------------------------------------------------------------- /examples/features/jhttptempl/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "forms.address": "Billing address", 3 | "forms.cart": "Your cart", 4 | "forms.checkout_title": "Checkout", 5 | "forms.continue": "Continue to checkout", 6 | "forms.description": "Brief description", 7 | "forms.first": "First", 8 | "forms.header": "Checkout", 9 | "forms.keep_infos": "Save this information for next time", 10 | "forms.lead": "Below is an example form built entirely with Bootstrap’s form controls. Each required form group has a validation state that can be triggered by attempting to submit the form without completing it.", 11 | "forms.name": "Name", 12 | "forms.password": "Password", 13 | "forms.product": "Product name", 14 | "forms.total": "Total %s", 15 | "home.choose_language": "Choose a language", 16 | "home.forms": "Forms", 17 | "home.header": "Spreak Tour", 18 | "home.lead": "Spreak is a flexible translation and humanization library for Go, based on the concepts behind gettext", 19 | "home.selection_field": "Language selection field", 20 | "home.title": "Welcome to Spreak", 21 | "not_found.great_user": "Nice to see you %s", 22 | "not_found.next_steps": "In the next steps we will see how we use spreak", 23 | "not_found.not_exist": "The page you are looking for does not exist.", 24 | "not_found.stupid_statement": { 25 | "one": "You will understand why Florian has a dog", 26 | "other": "You will understand why Florian has dogs" 27 | }, 28 | "not_found.welcome": "Welcome to the spreak tour", 29 | "not_found.wish": { 30 | "one": "We wish you a nice day", 31 | "other": "We wish you nice days" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /testdata/structure/de/LC_MESSAGES/a.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the Poedit package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Project 1.0\n" 9 | "Report-Msgid-Bugs-To: help@example.com\n" 10 | "POT-Creation-Date: 2021-06-03 11:36+0200\n" 11 | "PO-Revision-Date: 2022-04-29 23:17+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | msgid "id" 22 | msgstr "ID" 23 | 24 | msgid "empty translation" 25 | msgstr "" 26 | 27 | msgid "Test with special characters “%s”" 28 | msgstr "Test mit Sonderzeichen “%s”" 29 | 30 | msgid "%d day" 31 | msgid_plural "%d days" 32 | msgstr[0] "%d Tag" 33 | msgstr[1] "%d Tage" 34 | 35 | msgctxt "context" 36 | msgid "Test with context" 37 | msgstr "Test mit Context" 38 | 39 | msgctxt "context" 40 | msgid "%d result" 41 | msgid_plural "%d results" 42 | msgstr[0] "%d Ergebniss" 43 | msgstr[1] "%d Ergebnisse" 44 | 45 | msgid "Line %d of file “%s” is corrupted (not valid %s data)." 46 | msgstr "Zeile %d der Datei »%s« ist beschädigt (ungültige %s-Daten)." 47 | 48 | msgid "%d issue with the translation found." 49 | msgid_plural "%d issues with the translation found." 50 | msgstr[0] "Es wurde %d Problem mit der Übersetzung gefunden." 51 | msgstr[1] "Es wurden %d Probleme mit der Übersetzung gefunden." 52 | -------------------------------------------------------------------------------- /catalog/poplural/ast/parser_test.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestParser_Parse(t *testing.T) { 10 | 11 | t.Run("test invalid forms plural", func(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | form string 15 | }{ 16 | {"missing nplurals prefix", "=2; plural=(n != 1);"}, 17 | {"missing nplurals assign", "nplurals 2; plural=(n != 1);"}, 18 | {"first semicolon missing", "nplurals=2 plural=(n != 1);"}, 19 | {"missing semicolon missing", "nplurals=2 plural=(n != 1)"}, 20 | {"missing nplurals count", "nplurals= ; plural=(n != 1);"}, 21 | {"invalid nplurals count", "nplurals=t; plural=(n != 1);"}, 22 | {"open bracket right", "nplurals=2 plural=(n != 1;"}, 23 | {"open bracket left", "nplurals=2; plural=n != 1);"}, 24 | {"invalid neq sign", "nplurals=2; plural=(n !!= 1);"}, 25 | {"missing plural", "nplurals=2; =(n != 1);"}, 26 | {"missing plural assign", "nplurals=2; plural(n != 1);"}, 27 | {"invalid variable", "nplurals=2; plural=(a != 1);"}, 28 | {"invalid number", "nplurals=2; plural=(n != i);"}, 29 | {"invalid sign", "nplurals=2; plural=(n != 1)#;"}, 30 | {"invalid suffix", "nplurals=2; plural=(n != 1); more"}, 31 | {"missing colon", "nplurals=3; plural=(n==1 ? 0 n==2 ? 1 : 2);"}, 32 | } 33 | // 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | f, err := Parse(tt.form) 37 | assert.Error(t, err) 38 | assert.Nil(t, f) 39 | }) 40 | } 41 | 42 | }) 43 | } 44 | 45 | func TestMustParse(t *testing.T) { 46 | f := func() { 47 | MustParse("invalid plural formst") 48 | } 49 | assert.Panics(t, f) 50 | } 51 | -------------------------------------------------------------------------------- /catalog/po/wrap.go: -------------------------------------------------------------------------------- 1 | package po 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | 7 | "github.com/vorlif/spreak/internal/util" 8 | ) 9 | 10 | func wrapString(s string, pageWidth int) []string { 11 | if pageWidth <= 0 { 12 | return strings.Split(s, "\n") 13 | } 14 | 15 | lines := make([]string, 0, 10) 16 | 17 | currentLine := &util.StringBuffer{} 18 | var currentWordBuf util.StringBuffer 19 | var lastSpaceBuf util.StringBuffer 20 | 21 | for _, char := range s { 22 | if char == '\n' { 23 | if currentWordBuf.Len() == 0 { 24 | lastSpaceBuf.Reset() 25 | } 26 | lastSpaceBuf.WriteInto(currentLine) 27 | currentWordBuf.WriteInto(currentLine) 28 | lines = append(lines, currentLine.String()) 29 | currentLine.Reset() 30 | } else if unicode.IsSpace(char) && char != nbsp { 31 | if currentWordBuf.Len() > 0 { // We had a word before and now a space 32 | lastSpaceBuf.WriteInto(currentLine) 33 | currentWordBuf.WriteInto(currentLine) 34 | } 35 | 36 | lastSpaceBuf.WriteRune(char) 37 | } else { 38 | if currentLine.Len()+lastSpaceBuf.Len()+currentWordBuf.Len() >= pageWidth { 39 | if currentLine.Len() > 0 { 40 | lines = append(lines, currentLine.String()) 41 | currentLine.Reset() 42 | lastSpaceBuf.Reset() 43 | } 44 | 45 | if lastSpaceBuf.Len() > 0 { 46 | lastSpaceBuf.WriteInto(currentLine) 47 | currentWordBuf.WriteInto(currentLine) 48 | } 49 | } 50 | 51 | currentWordBuf.WriteRune(char) 52 | } 53 | } 54 | 55 | if currentWordBuf.Len() > 0 { 56 | lastSpaceBuf.WriteInto(currentLine) 57 | currentWordBuf.WriteInto(currentLine) 58 | } 59 | 60 | lines = append(lines, currentLine.String()) 61 | return lines 62 | } 63 | -------------------------------------------------------------------------------- /testdata/translation-test/de/LC_MESSAGES/a.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the Poedit package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Project 1.0\n" 9 | "Report-Msgid-Bugs-To: help@example.com\n" 10 | "POT-Creation-Date: 2021-06-03 11:36+0200\n" 11 | "PO-Revision-Date: 2022-04-29 23:17+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | msgid "id" 22 | msgstr "ID" 23 | 24 | msgid "empty translation" 25 | msgstr "" 26 | 27 | msgid "Test with special characters “%s”" 28 | msgstr "Test mit Sonderzeichen “%s”" 29 | 30 | msgid "%d day" 31 | msgid_plural "%d days" 32 | msgstr[0] "%d Tag" 33 | msgstr[1] "%d Tage" 34 | 35 | msgctxt "context" 36 | msgid "Test with context" 37 | msgstr "Test mit Context" 38 | 39 | msgctxt "context" 40 | msgid "%d result" 41 | msgid_plural "%d results" 42 | msgstr[0] "%d Ergebniss" 43 | msgstr[1] "%d Ergebnisse" 44 | 45 | msgid "Line %d of file “%s” is corrupted (not valid %s data)." 46 | msgstr "Zeile %d der Datei »%s« ist beschädigt (ungültige %s-Daten)." 47 | 48 | msgid "%d issue with the translation found." 49 | msgid_plural "%d issues with the translation found." 50 | msgstr[0] "Es wurde %d Problem mit der Übersetzung gefunden." 51 | msgstr[1] "Es wurden %d Probleme mit der Übersetzung gefunden." 52 | 53 | msgctxt "errors" 54 | msgid "failure" 55 | msgstr "fehler" -------------------------------------------------------------------------------- /catalog/catalog_test.go: -------------------------------------------------------------------------------- 1 | package catalog 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "golang.org/x/text/language" 9 | ) 10 | 11 | func TestErrMissingMessageId(t *testing.T) { 12 | err := &ErrMissingMessageID{ 13 | Language: language.Danish, 14 | Domain: "mydomain", 15 | Context: "mycontext", 16 | MsgID: "mymsgid", 17 | } 18 | assert.Error(t, err) 19 | assert.Contains(t, err.Error(), "spreak") 20 | 21 | assert.Contains(t, err.Error(), err.Language.String()) 22 | assert.Contains(t, err.Error(), err.Domain) 23 | assert.Contains(t, err.Error(), err.Context) 24 | assert.Contains(t, err.Error(), err.MsgID) 25 | } 26 | 27 | func TestErrMissingContext(t *testing.T) { 28 | err := &ErrMissingContext{} 29 | assert.Error(t, err) 30 | assert.Contains(t, err.Error(), "spreak") 31 | 32 | err.Language = language.Afrikaans 33 | assert.Contains(t, err.Error(), err.Language.String()) 34 | 35 | err.Domain = "mydomain" 36 | assert.Contains(t, err.Error(), err.Domain) 37 | 38 | err.Context = "mycontext" 39 | assert.Contains(t, err.Error(), err.Context) 40 | } 41 | 42 | func TestErrMissingTranslation(t *testing.T) { 43 | err := &ErrMissingTranslation{ 44 | Language: language.Danish, 45 | Domain: "mydomain", 46 | Context: "mycontext", 47 | MsgID: "mymsgid", 48 | Idx: 1022, 49 | } 50 | assert.Error(t, err) 51 | assert.Contains(t, err.Error(), "spreak") 52 | 53 | assert.Contains(t, err.Error(), err.Language.String()) 54 | assert.Contains(t, err.Error(), err.Domain) 55 | assert.Contains(t, err.Error(), err.Context) 56 | assert.Contains(t, err.Error(), err.MsgID) 57 | assert.Contains(t, err.Error(), strconv.Itoa(err.Idx)) 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - master 9 | - main 10 | pull_request: 11 | 12 | jobs: 13 | 14 | golangci: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v5 18 | - uses: actions/setup-go@v6 19 | with: 20 | go-version: '1.23' 21 | cache: false 22 | - name: lint 23 | uses: golangci/golangci-lint-action@v8 24 | with: 25 | version: latest 26 | 27 | coverage: 28 | name: 'Coverage' 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | - uses: actions/checkout@v5 33 | - uses: actions/setup-go@v6 34 | with: 35 | go-version: 'stable' 36 | 37 | - name: Create coverage report 38 | run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... 39 | 40 | - name: Upload coverage report 41 | uses: codecov/codecov-action@v5 42 | with: 43 | token: ${{ secrets.CODECOV_TOKEN }} 44 | fail_ci_if_error: true 45 | 46 | test: 47 | strategy: 48 | matrix: 49 | platform: 50 | - ubuntu 51 | - macOS 52 | - windows 53 | go: 54 | - 23 55 | - 24 56 | - 25 57 | name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x' 58 | runs-on: ${{ matrix.platform }}-latest 59 | steps: 60 | - uses: actions/checkout@v5 61 | - uses: actions/setup-go@v6 62 | with: 63 | go-version: 1.${{ matrix.go }}.x 64 | 65 | - name: Build 66 | run: go build -tags timetzdata -v ./... 67 | 68 | - name: Test 69 | run: go test -tags timetzdata -v ./... 70 | -------------------------------------------------------------------------------- /catalog/poplural/ast/compile.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | func CompileToString(forms *Forms) string { 10 | return fmt.Sprintf("nplurals=%d; plural=%s;", forms.NPlurals, compileNode(forms.Root)) 11 | } 12 | 13 | func compileNode(node Node) string { 14 | switch v := node.(type) { 15 | case *ValueExpr: 16 | return strconv.FormatInt(v.Value, 10) 17 | case *OperandExpr: 18 | return "n" 19 | case *QuestionMarkExpr: 20 | cond := compileNode(v.Cond) 21 | then := compileNode(v.T) 22 | other := compileNode(v.F) 23 | 24 | if len(cond) > 1 && (!strings.HasPrefix(cond, "(") || !strings.HasSuffix(cond, ")")) { 25 | cond = "(" + cond + ")" 26 | } 27 | if len(then) > 1 { 28 | then = "(" + then + ")" 29 | } 30 | if len(other) > 1 { 31 | other = "(" + other + ")" 32 | } 33 | 34 | return fmt.Sprintf("%s ? %s : %s", cond, then, other) 35 | case *BinaryExpr: 36 | switch v.Type() { 37 | case LogicalAnd: 38 | return compileNode(v.X) + " && " + compileNode(v.Y) 39 | case LogicalOr: 40 | return fmt.Sprintf("(%s || %s)", compileNode(v.X), compileNode(v.Y)) 41 | case Equal: 42 | return compileNode(v.X) + " == " + compileNode(v.Y) 43 | case NotEqual: 44 | return compileNode(v.X) + " != " + compileNode(v.Y) 45 | case Greater: 46 | return compileNode(v.X) + " > " + compileNode(v.Y) 47 | case GreaterOrEqual: 48 | return compileNode(v.X) + " >= " + compileNode(v.Y) 49 | case Less: 50 | return compileNode(v.X) + " < " + compileNode(v.Y) 51 | case LessOrEqual: 52 | return compileNode(v.X) + " <= " + compileNode(v.Y) 53 | case Reminder: 54 | return compileNode(v.X) + " % " + compileNode(v.Y) 55 | } 56 | } 57 | return "" 58 | } 59 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | 2 | Here you will find a number of helpful examples of how you can use spreak. 3 | 4 | ## Setup 5 | 6 | Follow these setup to run the `helloworld` example: 7 | 8 | 1. Get the code 9 | 10 | ```shell 11 | $ git clone https://github.com/vorlif/spreak 12 | ``` 13 | 14 | 2. Run the code 15 | 16 | ```shell 17 | cd spreak/examples/helloworld 18 | go run main.go 19 | ``` 20 | 21 | ## What is included? 22 | 23 | Unless otherwise required for the demonstration, all `.pot` files and the associated translations are 24 | stored in the folder `locale`. 25 | 26 | * [helloworld](./helloworld/main.go) 27 | * Simple example to demonstrate how spreak can be used 28 | * xspreak command `xspreak -e -D helloworld/ -p locale/ -d helloworld` 29 | 30 | * [dayinfo](./dayinfo/main.go) 31 | * Advanced example to demonstrate how spreak can be used with xspreak 32 | * xspreak command `xspreak -e -D dayinfo/ -p locale/ -d dayinfo` 33 | 34 | * [httptempl](features/httptempl): Example of how to use spreak with a web server and `http/templates` 35 | 36 | * [jhttptempl](features/jhttptempl): Example of how to use spreak with a web server, json and `http/templates` 37 | 38 | * [embed](./features/embed/main.go): Example how spreak can be used with the embed api 39 | 40 | * [errors](./features/errors/main.go): Example how errors can be translated with spreak and xspreak 41 | 42 | * [loaders](./features/loaders/main.go): Example how to load PO files from other sources 43 | 44 | * [resolver](./features/resolver/main.go): Example how to resolve the path to a file with translations. 45 | 46 | * [printer](./features/printer/main.go): Example how to use your own printer 47 | 48 | * [decoder](./features/decoder): Example of implementation of a decoder and a catalog for importing JSON files for translation. 49 | -------------------------------------------------------------------------------- /examples/locale/dayinfo.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-05-09 13:55+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: ../dayinfo/main.go:24 21 | msgctxt "errors" 22 | msgid "the name of the holiday has an invalid format" 23 | msgstr "" 24 | 25 | #: ../dayinfo/main.go:54 26 | msgid "System language detected: %v" 27 | msgstr "" 28 | 29 | #: ../dayinfo/main.go:63 30 | msgid "Monday" 31 | msgstr "" 32 | 33 | #: ../dayinfo/main.go:63 34 | msgid "Tuesday" 35 | msgstr "" 36 | 37 | #: ../dayinfo/main.go:63 38 | msgid "Wednesday" 39 | msgstr "" 40 | 41 | #: ../dayinfo/main.go:63 42 | msgid "Thursday" 43 | msgstr "" 44 | 45 | #: ../dayinfo/main.go:63 46 | msgid "Friday" 47 | msgstr "" 48 | 49 | #: ../dayinfo/main.go:63 50 | msgid "Saturday" 51 | msgstr "" 52 | 53 | #: ../dayinfo/main.go:63 54 | msgid "Sunday" 55 | msgstr "" 56 | 57 | #: ../dayinfo/main.go:70 58 | msgid "The weekday is %s\n" 59 | msgstr "" 60 | 61 | #: ../dayinfo/main.go:75 62 | #, range: 1..7 63 | msgid "%d day left until Friday" 64 | msgid_plural "%d days left until Friday" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | #: ../dayinfo/main.go:78 69 | msgid "Christmas" 70 | msgstr "" 71 | 72 | #: ../dayinfo/main.go:79 73 | msgid "An example of a holiday is %s\n" 74 | msgstr "" 75 | 76 | -------------------------------------------------------------------------------- /internal/cast/number.go: -------------------------------------------------------------------------------- 1 | package cast 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | ) 9 | 10 | func ToNumber(n any) (float64, error) { 11 | n = Indirect(n) 12 | if n == nil { 13 | return 0, errors.New("number is nil") 14 | } 15 | 16 | switch nt := n.(type) { 17 | case uint: 18 | return float64(nt), nil 19 | case uint8: 20 | return float64(nt), nil 21 | case uint16: 22 | return float64(nt), nil 23 | case uint32: 24 | return float64(nt), nil 25 | case uint64: 26 | return float64(nt), nil 27 | case int: 28 | return float64(nt), nil 29 | case int8: 30 | return float64(nt), nil 31 | case int16: 32 | return float64(nt), nil 33 | case int32: 34 | return float64(nt), nil 35 | case int64: 36 | return float64(nt), nil 37 | case float32: 38 | return float64(nt), nil 39 | case float64: 40 | return nt, nil 41 | case complex64: 42 | return float64(real(nt)), nil 43 | case complex128: 44 | return real(nt), nil 45 | case string: 46 | res, err := strconv.ParseFloat(nt, 64) 47 | if err != nil { 48 | return 0, fmt.Errorf("unable to cast %#v of type %T to float64", n, n) 49 | } 50 | return res, nil 51 | } 52 | 53 | if num, errC := ToNumber(fmt.Sprintf("%d", n)); errC == nil { 54 | return num, nil 55 | } 56 | 57 | return ToNumber(fmt.Sprintf("%v", n)) 58 | } 59 | 60 | // Indirect returns the value, after dereferencing as many times 61 | // as necessary to reach the base type (or nil). 62 | // From html/template/content.go 63 | // Copyright 2011 The Go Authors. All rights reserved. 64 | func Indirect(a any) any { 65 | if a == nil { 66 | return nil 67 | } 68 | if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr { 69 | // Avoid creating a reflect.Value if it's not a pointer. 70 | return a 71 | } 72 | v := reflect.ValueOf(a) 73 | for v.Kind() == reflect.Ptr && !v.IsNil() { 74 | v = v.Elem() 75 | } 76 | return v.Interface() 77 | } 78 | -------------------------------------------------------------------------------- /humanize/humanizer_test.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "golang.org/x/text/language" 10 | 11 | "github.com/vorlif/spreak" 12 | ) 13 | 14 | var testdataDir = filepath.FromSlash("../testdata/humanize") 15 | var deLocaleDir = filepath.FromSlash("./locale/de") 16 | var esLocaleDir = filepath.FromSlash("./locale/es") 17 | 18 | var testGermanLocaleData = &LocaleData{ 19 | Lang: language.German, 20 | Fs: os.DirFS(deLocaleDir), 21 | Format: &FormatData{ 22 | DateFormat: "j. F Y", 23 | TimeFormat: "H:i", 24 | DateTimeFormat: "j. F Y H:i", 25 | YearMonthFormat: "F Y", 26 | MonthDayFormat: "j. F", 27 | ShortDateFormat: "d.m.Y", 28 | ShortDatetimeFormat: "d.m.Y H:i", 29 | FirstDayOfWeek: 1, 30 | }, 31 | } 32 | 33 | func createNewParcel(_ *testing.T) *Collection { 34 | es := &LocaleData{Lang: language.Spanish, Fs: os.DirFS(esLocaleDir)} 35 | 36 | return MustNew(WithLocale(testGermanLocaleData, es)) 37 | } 38 | 39 | func createGermanHumanizer(t *testing.T) *Humanizer { 40 | p := createNewParcel(t) 41 | return p.CreateHumanizer(language.German) 42 | } 43 | 44 | func createSourceHumanizer(t *testing.T) *Humanizer { 45 | p := createNewParcel(t) 46 | return p.CreateHumanizer(language.English) 47 | } 48 | 49 | func TestNew(t *testing.T) { 50 | t.Run("test same domain returns error", func(t *testing.T) { 51 | collection, err := New(WithBundleOption(spreak.WithDomainPath(djangoDomain, "../testdata/humanize"))) 52 | assert.Error(t, err) 53 | assert.Contains(t, err.Error(), "django") 54 | assert.Nil(t, collection) 55 | }) 56 | 57 | t.Run("test MuseNew with error panics", func(t *testing.T) { 58 | f := func() { 59 | _ = MustNew(WithBundleOption(spreak.WithDomainPath(djangoDomain, "../testdata/humanize"))) 60 | } 61 | assert.Panics(t, f) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /examples/features/resolver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | 9 | "golang.org/x/text/language" 10 | 11 | "github.com/vorlif/spreak" 12 | ) 13 | 14 | // A resolver resolves the file path from which the file is to be loaded. 15 | type myResolver struct{} 16 | 17 | var _ spreak.Resolver = (*myResolver)(nil) 18 | 19 | func (myResolver) Resolve(fsys fs.FS, extensions string, lang language.Tag, domain string) (string, error) { 20 | // We create the path where the file should be located 21 | path := filepath.Join("locale", lang.String(), domain+extensions) 22 | 23 | // Verify if the file exists 24 | if _, err := fs.Stat(fsys, path); err == nil { 25 | // And pass the loader the path 26 | return path, nil 27 | } 28 | 29 | // If the file does not exist, but we want to continue without the file, we return os.ErrNotExist. 30 | return "", os.ErrNotExist 31 | } 32 | 33 | func main() { 34 | // We would like to define ourselves how to resolve the paths and filenames to the files. 35 | // Therefore, we need to create our own FilesystemLoader 36 | fsLoader, errFS := spreak.NewFilesystemLoader( 37 | spreak.WithResolver(&myResolver{}), 38 | spreak.WithPath("../../"), 39 | ) 40 | if errFS != nil { 41 | panic(errFS) 42 | } 43 | 44 | bundle, err := spreak.NewBundle( 45 | spreak.WithSourceLanguage(language.English), 46 | spreak.WithDefaultDomain("helloworld"), 47 | // We now load the domains via our own FilesystemLoader, which uses our resolver. 48 | spreak.WithDomainLoader("helloworld", fsLoader), 49 | spreak.WithRequiredLanguage(language.Spanish), 50 | spreak.WithLanguage(language.German, language.French), 51 | ) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | fmt.Println(bundle.SupportedLanguages()) 57 | 58 | t := spreak.NewLocalizer(bundle, language.Spanish) 59 | fmt.Println(t.Get("Hello world")) 60 | // Output: Hola Mundo 61 | } 62 | -------------------------------------------------------------------------------- /catalog/example_test.go: -------------------------------------------------------------------------------- 1 | package catalog 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "golang.org/x/text/language" 8 | 9 | "github.com/vorlif/spreak/catalog/cldrplural" 10 | ) 11 | 12 | func ExampleNewJSONCatalog() { 13 | cat := NewJSONCatalog(language.German, "domain") 14 | data := []byte(`{ 15 | "help": "Hilfe", 16 | "car": { 17 | "one": "Auto", 18 | "other": "Autos" 19 | } 20 | }`) 21 | 22 | if err := json.Unmarshal(data, cat); err != nil { 23 | panic(err) 24 | } 25 | 26 | tr, err := cat.Lookup("", "help") 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | fmt.Println(tr) 32 | // Output: 33 | // Hilfe 34 | } 35 | 36 | func ExampleNewJSONCatalogWithMessages() { 37 | messages := make(JSONMessages) 38 | messages["car"] = &JSONMessage{ 39 | Translations: map[cldrplural.Category]string{ 40 | cldrplural.One: "Car", 41 | cldrplural.Other: "Cars", 42 | }, 43 | } 44 | messages["help_ctx"] = &JSONMessage{ 45 | Translations: map[cldrplural.Category]string{ 46 | cldrplural.Other: "Help", 47 | }, 48 | } 49 | 50 | cat, err := NewJSONCatalogWithMessages(language.English, "", messages) 51 | if err != nil { 52 | panic(err) 53 | } 54 | fmt.Println(cat.Lookup("", "car")) 55 | 56 | res, err := json.Marshal(cat) 57 | if err != nil { 58 | panic(err) 59 | } 60 | fmt.Println(string(res)) 61 | 62 | // Output: 63 | // Cars 64 | // {"car":{"one":"Car","other":"Cars"},"help_ctx":"Help"} 65 | } 66 | 67 | func ExampleApplyPluralCategoriesToJSONMessage() { 68 | msg := &JSONMessage{ 69 | Translations: map[cldrplural.Category]string{ 70 | cldrplural.One: "Car", 71 | cldrplural.Other: "Cars", 72 | }, 73 | } 74 | 75 | fmt.Println("Before", msg.Translations) 76 | 77 | ApplyPluralCategoriesToJSONMessage(language.Polish, msg) 78 | 79 | fmt.Println("After", msg.Translations) 80 | 81 | // Output: 82 | // Before map[One:Car Other:Cars] 83 | // After map[One:Car Few: Many: Other:Cars] 84 | } 85 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package spreak_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/text/language" 7 | 8 | "github.com/vorlif/spreak" 9 | "github.com/vorlif/spreak/localize" 10 | ) 11 | 12 | func ExampleNewBundle() { 13 | bundle, err := spreak.NewBundle( 14 | spreak.WithSourceLanguage(language.English), 15 | spreak.WithDefaultDomain("helloworld"), 16 | spreak.WithDomainPath("helloworld", "./examples/locale/"), 17 | spreak.WithLanguage(language.German, language.Spanish, language.French), 18 | ) 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | t := spreak.NewLocalizer(bundle, language.Spanish) 24 | 25 | fmt.Println(t.Get("Hello world")) 26 | // Output: 27 | // Hola Mundo 28 | } 29 | 30 | func ExampleLocalizer_Get() { 31 | bundle, err := spreak.NewBundle( 32 | spreak.WithSourceLanguage(language.English), 33 | spreak.WithDefaultDomain("helloworld"), 34 | spreak.WithDomainPath("helloworld", "./examples/locale/"), 35 | spreak.WithLanguage(language.German, language.Spanish, language.French), 36 | ) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | t := spreak.NewLocalizer(bundle, language.Spanish) 42 | 43 | fmt.Println(t.Get("Hello world")) 44 | // Output: 45 | // Hola Mundo 46 | } 47 | 48 | func ExampleLocalizer_Localize() { 49 | bundle, err := spreak.NewBundle( 50 | spreak.WithDefaultDomain("helloworld"), 51 | spreak.WithDomainPath("helloworld", "./examples/locale/"), 52 | spreak.WithLanguage(language.German, language.Spanish, language.French), 53 | ) 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | t := spreak.NewLocalizer(bundle, language.Spanish) 59 | 60 | msg := &localize.Message{Singular: "Hello world"} 61 | fmt.Println(t.Localize(msg)) 62 | // Output: 63 | // Hola Mundo 64 | } 65 | 66 | func ExampleExpandLanguage() { 67 | expanded := spreak.ExpandLanguage(language.MustParse("zh-Hans")) 68 | fmt.Println(expanded) 69 | // Output: 70 | // [zh-Hans zh_Hans zh-CN zh_CN zho zh] 71 | } 72 | -------------------------------------------------------------------------------- /examples/features/jhttptempl/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | {{.i18n.Tr "home.title" }} 9 | 10 | 11 | 12 |
13 |
14 | 16 |

{{.i18n.Tr "home.header"}}

17 |
18 |

19 | {{.i18n.Tr "home.lead" }} 20 |

21 | 25 |
26 | 27 |
28 | 37 |
38 |
39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/features/httptempl/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | {{.i18n.Tr "Spreak Tour" }} 9 | 10 | 11 | 12 |
13 |
14 | 16 |

{{.i18n.Tr "Welcome to Spreak"}}

17 |
18 |

19 | {{.i18n.Tr "Spreak is a flexible translation and humanization library for Go, based on the concepts behind gettext" }} 20 |

21 |
22 | {{$.i18n.Tr "Forms"}} 23 | 404 24 |
25 |
26 | 27 |
28 | 36 |
37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/locale/de/dayinfo.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE VERSION package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-08 00:07+0200\n" 11 | "PO-Revision-Date: 2022-05-08 00:08+0200\n" 12 | "Last-Translator: Florian Vogt\n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 3.0.1\n" 20 | 21 | #: ../dayinfo/main.go:24 22 | msgctxt "errors" 23 | msgid "the name of the holiday has an invalid format" 24 | msgstr "der Name des Feiertags hat ein ungültiges Format" 25 | 26 | #: ../dayinfo/main.go:54 27 | msgid "System language detected: %v" 28 | msgstr "Systemsprache erkannt: %v" 29 | 30 | #: ../dayinfo/main.go:63 31 | msgid "Friday" 32 | msgstr "Freitag" 33 | 34 | #: ../dayinfo/main.go:63 35 | msgid "Saturday" 36 | msgstr "Samstag" 37 | 38 | #: ../dayinfo/main.go:63 39 | msgid "Monday" 40 | msgstr "Montag" 41 | 42 | #: ../dayinfo/main.go:63 43 | msgid "Thursday" 44 | msgstr "Donnerstag" 45 | 46 | #: ../dayinfo/main.go:63 47 | msgid "Wednesday" 48 | msgstr "Mittwoch" 49 | 50 | #: ../dayinfo/main.go:63 51 | msgid "Tuesday" 52 | msgstr "Dienstag" 53 | 54 | #: ../dayinfo/main.go:63 55 | msgid "Sunday" 56 | msgstr "Sonntag" 57 | 58 | #: ../dayinfo/main.go:70 59 | msgid "The weekday is %s\n" 60 | msgstr "Der Wochentag ist %s\n" 61 | 62 | #: ../dayinfo/main.go:75 63 | #, range: 1..7 64 | msgid "%d day left until Friday" 65 | msgid_plural "%d days left until Friday" 66 | msgstr[0] "noch %d Tag bis Freitag" 67 | msgstr[1] "noch %d Tage bis Freitag" 68 | 69 | #: ../dayinfo/main.go:78 70 | msgid "Christmas" 71 | msgstr "Weihnachten" 72 | 73 | #: ../dayinfo/main.go:79 74 | msgid "An example of a holiday is %s\n" 75 | msgstr "Ein Beispiel für einen Feiertag ist %s\n" 76 | -------------------------------------------------------------------------------- /humanize/dates.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | var ( 4 | weekdays = map[int]string{ 5 | 0: "Monday", 6 | 1: "Tuesday", 7 | 2: "Wednesday", 8 | 3: "Thursday", 9 | 4: "Friday", 10 | 5: "Saturday", 11 | 6: "Sunday", 12 | } 13 | 14 | weekdaysAbbr = map[int]string{ 15 | 0: "Mon", 16 | 1: "Tue", 17 | 2: "Wed", 18 | 3: "Thu", 19 | 4: "Fri", 20 | 5: "Sat", 21 | 6: "Sun", 22 | } 23 | 24 | months = map[int]string{ 25 | 1: "January", 26 | 2: "February", 27 | 3: "March", 28 | 4: "April", 29 | 5: "May", 30 | 6: "June", 31 | 7: "July", 32 | 8: "August", 33 | 9: "September", 34 | 10: "October", 35 | 11: "November", 36 | 12: "December", 37 | } 38 | 39 | months3 = map[int]string{ 40 | 1: "jan", 41 | 2: "feb", 42 | 3: "mar", 43 | 4: "apr", 44 | 5: "may", 45 | 6: "jun", 46 | 7: "jul", 47 | 8: "aug", 48 | 9: "sep", 49 | 10: "oct", 50 | 11: "nov", 51 | 12: "dec", 52 | } 53 | 54 | monthsAp = map[int]gettextEntry{ // month names in Associated Press style 55 | 1: {"abbrev. month", "Jan.", ""}, 56 | 2: {"abbrev. month", "Feb.", ""}, 57 | 3: {"abbrev. month", "March", ""}, 58 | 4: {"abbrev. month", "April", ""}, 59 | 5: {"abbrev. month", "May", ""}, 60 | 6: {"abbrev. month", "June", ""}, 61 | 7: {"abbrev. month", "July", ""}, 62 | 8: {"abbrev. month", "Aug.", ""}, 63 | 9: {"abbrev. month", "Sept.", ""}, 64 | 10: {"abbrev. month", "Oct.", ""}, 65 | 11: {"abbrev. month", "Nov.", ""}, 66 | 12: {"abbrev. month", "Dec.", ""}, 67 | } 68 | 69 | monthsAlt = map[int]gettextEntry{ // required for long date representation by some locales 70 | 1: {"alt. month", "January", ""}, 71 | 2: {"alt. month", "February", ""}, 72 | 3: {"alt. month", "March", ""}, 73 | 4: {"alt. month", "April", ""}, 74 | 5: {"alt. month", "May", ""}, 75 | 6: {"alt. month", "June", ""}, 76 | 7: {"alt. month", "July", ""}, 77 | 8: {"alt. month", "August", ""}, 78 | 9: {"alt. month", "September", ""}, 79 | 10: {"alt. month", "October", ""}, 80 | 11: {"alt. month", "November", ""}, 81 | 12: {"alt. month", "December", ""}, 82 | } 83 | ) 84 | -------------------------------------------------------------------------------- /internal/cast/time_test.go: -------------------------------------------------------------------------------- 1 | package cast 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestToTime(t *testing.T) { 12 | now := time.Now() 13 | 14 | t.Run("test errors", func(t *testing.T) { 15 | now := time.Now() 16 | tests := []struct { 17 | name string 18 | arg interface{} 19 | wantErr assert.ErrorAssertionFunc 20 | }{ 21 | {"time", now, assert.NoError}, 22 | {"zero time", time.Time{}, assert.NoError}, 23 | {"positive duration", 5 * time.Minute, assert.NoError}, 24 | {"negative duration", -5 * time.Hour, assert.NoError}, 25 | {"number to time", 1_000, assert.NoError}, 26 | {"pointer", &now, assert.NoError}, 27 | {"nil", nil, assert.Error}, 28 | {"invalid time", "string", assert.Error}, 29 | } 30 | 31 | for _, tt := range tests { 32 | t.Run(tt.name, func(t *testing.T) { 33 | d, err := ToTime(tt.arg) 34 | tt.wantErr(t, err, fmt.Sprintf("ToTime(%v)", tt.arg)) 35 | assert.NotNil(t, d) 36 | }) 37 | } 38 | }) 39 | 40 | t.Run("test simple time input", func(t *testing.T) { 41 | d, err := ToTime(now) 42 | assert.NoError(t, err) 43 | assert.Equal(t, now, d) 44 | }) 45 | 46 | t.Run("test pointer", func(t *testing.T) { 47 | d, err := ToTime(&now) 48 | assert.NoError(t, err) 49 | assert.Equal(t, now, d) 50 | }) 51 | 52 | t.Run("test numbers", func(t *testing.T) { 53 | d, err := ToTime(1_000) 54 | assert.NoError(t, err) 55 | assert.Equal(t, int64(1000), d.Unix()) 56 | }) 57 | 58 | t.Run("test empty", func(t *testing.T) { 59 | d, err := ToTime(time.Time{}) 60 | assert.NoError(t, err) 61 | assert.True(t, d.IsZero()) 62 | assert.Equal(t, time.Time{}, d) 63 | }) 64 | 65 | t.Run("test duration", func(t *testing.T) { 66 | d, err := ToTime(5 * time.Minute) 67 | assert.NoError(t, err) 68 | mins := time.Until(d).Minutes() 69 | assert.Truef(t, mins >= 4.75 && mins <= 5.25, fmt.Sprintf("%v", mins)) 70 | 71 | d, err = ToTime(-10 * time.Hour) 72 | assert.NoError(t, err) 73 | hours := time.Since(d).Hours() 74 | assert.Truef(t, hours >= 9.9 && hours <= 10.1, fmt.Sprintf("%v", hours)) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /examples/features/jhttptempl/not_found.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | ) 7 | 8 | const Title = "not_found.welcome" 9 | 10 | // In the following text, the strings are extracted by xstreak because it has the tag "xspreak: template". 11 | // xspreak: template 12 | var notFoundTemplate = template.Must(template.New("").Parse(` 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 |
24 |
25 | 27 |

{{ .i18n.Tr "not_found.not_exist" }}

28 |
29 | {{range .Paragraphs}} 30 |

{{.}}

31 | {{end}} 32 |

{{ .i18n.TrN "not_found.wish" 2}}

33 |

{{ .i18n.Tr "not_found.great_user" .User}}

34 |

{{.Title}}

35 |
36 |
37 |
38 | 39 | 40 | 41 | `)) 42 | 43 | // NotFound is a simple handler for not found pages, which gives a nonsense feedback. 44 | func (h *handler) NotFound(w http.ResponseWriter, r *http.Request) { 45 | localizer := h.createLocalizer(r) 46 | username := r.FormValue("name") 47 | if username == "" { 48 | username = "John" 49 | } 50 | 51 | paragraphs := []string{ 52 | // TRANSLATORS: spreak is the name of the application 53 | localizer.Get("not_found.next_steps"), 54 | localizer.NGet("not_found.stupid_statement", 1), 55 | } 56 | 57 | err := notFoundTemplate.Execute(w, map[string]any{ 58 | "Title": localizer.Get(Title), 59 | "User": username, 60 | "Paragraphs": paragraphs, 61 | "i18n": NewI18N(localizer), 62 | }) 63 | if err != nil { 64 | panic(err) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /catalog/cldrplural/cldr_test.go: -------------------------------------------------------------------------------- 1 | package cldrplural 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestMustNewOperands(t *testing.T) { 11 | t.Run("panics on error", func(t *testing.T) { 12 | f := func() { MustNewOperands("") } 13 | assert.Panics(t, f) 14 | }) 15 | } 16 | 17 | func TestExtractOperands(t *testing.T) { 18 | tests := []struct { 19 | src string 20 | n float64 21 | i int64 22 | v int64 23 | w int64 24 | f int64 25 | t int64 26 | c int64 27 | }{ 28 | {"1", 1, 1, 0, 0, 0, 0, 0}, 29 | {"1.", 1, 1, 0, 0, 0, 0, 0}, 30 | {"1.0", 1, 1, 1, 0, 0, 0, 0}, 31 | {"1.00", 1, 1, 2, 0, 0, 0, 0}, 32 | {"1.3", 1.3, 1, 1, 1, 3, 3, 0}, 33 | {"1.30", 1.3, 1, 2, 1, 30, 3, 0}, 34 | {"1.03", 1.03, 1, 2, 2, 3, 3, 0}, 35 | {"1.230", 1.23, 1, 3, 2, 230, 23, 0}, 36 | {"1200000", 1200000, 1200000, 0, 0, 0, 0, 0}, 37 | {"1.2c6", 1200000, 1200000, 0, 0, 0, 0, 6}, 38 | {"123c6", 123000000, 123000000, 0, 0, 0, 0, 6}, 39 | {"123c5", 12300000, 12300000, 0, 0, 0, 0, 5}, 40 | {"1200.50", 1200.5, 1200, 2, 1, 50, 5, 0}, 41 | {"1.20050c3", 1200.5, 1200, 2, 1, 50, 5, 3}, 42 | } 43 | 44 | for _, tt := range tests { 45 | op, err := NewOperands(tt.src) 46 | require.NoError(t, err) 47 | assert.Equalf(t, tt.n, op.N, "N %s", tt.src) 48 | assert.Equalf(t, tt.i, op.I, "I %s", tt.src) 49 | assert.Equalf(t, tt.v, op.V, "V %s", tt.src) 50 | assert.Equalf(t, tt.w, op.W, "W %s", tt.src) 51 | assert.Equalf(t, tt.f, op.F, "F %s", tt.src) 52 | assert.Equalf(t, tt.t, op.T, "T %s", tt.src) 53 | assert.Equalf(t, tt.c, op.C, "C %s", tt.src) 54 | } 55 | 56 | assert.Equal(t, -3, -3%5) 57 | assert.Equal(t, -3, -3%-5) 58 | assert.Equal(t, 3, 3%5) 59 | } 60 | 61 | func Test_formatExponent(t *testing.T) { 62 | tests := []struct { 63 | input string 64 | c int 65 | want string 66 | }{ 67 | {"1.2", 6, "1200000"}, 68 | {"123", 6, "123000000"}, 69 | {"123", 5, "12300000"}, 70 | {"1.20050", 3, "1200.50"}, 71 | } 72 | 73 | for _, tt := range tests { 74 | t.Run(tt.input, func(t *testing.T) { 75 | assert.Equalf(t, tt.want, shiftDecimalPoint(tt.input, tt.c), "shiftDecimalPoint(%q, %d) = %s", tt.input, tt.c, tt.want) 76 | }) 77 | 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/features/httptempl/not_found.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | ) 7 | 8 | const Title = "Welcome to the spreak tour" 9 | 10 | // In the following text, the strings are extracted by xstreak because it has the tag "xspreak: template". 11 | // xspreak: template 12 | var notFoundTemplate = template.Must(template.New("").Parse(` 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 |
24 |
25 | 27 |

{{ .i18n.Tr "The page you are looking for does not exist." }}

28 |
29 | {{range .Paragraphs}} 30 |

{{.}}

31 | {{end}} 32 |

{{ .i18n.Tr "Nice to see you %s" .User}}

33 |

{{.Title}}

34 |
35 |
36 |
37 | 38 | 39 | 40 | `)) 41 | 42 | // NotFound is a simple handler for not found pages, which gives a nonsense feedback. 43 | func (h *handler) NotFound(w http.ResponseWriter, r *http.Request) { 44 | localizer := h.createLocalizer(r) 45 | username := r.FormValue("name") 46 | if username == "" { 47 | username = "John" 48 | } 49 | 50 | paragraphs := []string{ 51 | // TRANSLATORS: spreak is the name of the application 52 | localizer.Get("In the next steps we will see how we use spreak"), 53 | localizer.NGet("You will understand why Florian has a dog", "You will understand why Florian has dogs", 1), 54 | } 55 | 56 | err := notFoundTemplate.Execute(w, map[string]any{ 57 | "Title": localizer.Get(Title), 58 | "User": username, 59 | "Paragraphs": paragraphs, 60 | "i18n": NewI18N(localizer), 61 | }) 62 | if err != nil { 63 | panic(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/cast/number_test.go: -------------------------------------------------------------------------------- 1 | package cast 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestToNumber(t *testing.T) { 13 | s := "5.5" 14 | i := 10 15 | i8 := int8(8) 16 | i16 := int16(16) 17 | i32 := int32(32) 18 | i64 := int64(64) 19 | ui8 := uint8(8) 20 | ui16 := uint16(16) 21 | ui32 := uint32(32) 22 | ui64 := uint64(64) 23 | wd := time.Now().Weekday() 24 | 25 | tests := []struct { 26 | name string 27 | arg interface{} 28 | wantRes float64 29 | wantErr assert.ErrorAssertionFunc 30 | }{ 31 | {"int", 1, 1, assert.NoError}, 32 | {"int8", int8(1), 1, assert.NoError}, 33 | {"int16", int16(1), 1, assert.NoError}, 34 | {"int32", int32(1), 1, assert.NoError}, 35 | {"int64", int64(1), 1, assert.NoError}, 36 | {"int8 pointer", &i8, float64(i8), assert.NoError}, 37 | {"int16 pointer", &i16, float64(i16), assert.NoError}, 38 | {"int32 pointer", &i32, float64(i32), assert.NoError}, 39 | {"int64 pointer", &i64, float64(i64), assert.NoError}, 40 | {"uint", uint(5), 5, assert.NoError}, 41 | {"uint8", uint8(5), 5, assert.NoError}, 42 | {"uint16", uint16(5), 5, assert.NoError}, 43 | {"uint32", uint32(5), 5, assert.NoError}, 44 | {"uint64", uint64(5), 5, assert.NoError}, 45 | {"uint8 pointer", &ui8, float64(ui8), assert.NoError}, 46 | {"uint16 pointer", &ui16, float64(ui16), assert.NoError}, 47 | {"uint32 pointer", &ui32, float64(ui32), assert.NoError}, 48 | {"uint64 pointer", &ui64, float64(ui64), assert.NoError}, 49 | {"int pointer", &i, float64(i), assert.NoError}, 50 | {"float64", 1.11111, 1.11111, assert.NoError}, 51 | {"int string", "5", 5, assert.NoError}, 52 | {"string", s, 5.5, assert.NoError}, 53 | {"string pointer", &s, 5.5, assert.NoError}, 54 | {"weekday", wd, float64(wd), assert.NoError}, 55 | {"weekday pointer", wd, float64(wd), assert.NoError}, 56 | {"nil pointer", nil, 0, assert.Error}, 57 | {"invalid string", "hello world", 0, assert.Error}, 58 | {"json number", json.Number("1234"), 1234, assert.NoError}, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | gotRes, err := ToNumber(tt.arg) 63 | if !tt.wantErr(t, err, fmt.Sprintf("ToNumber(%v)", tt.arg)) { 64 | return 65 | } 66 | assert.Equalf(t, tt.wantRes, gotRes, "ToNumber(%v)", tt.arg) 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /catalog/po/wrap_test.go: -------------------------------------------------------------------------------- 1 | // Original work Copyright (c) 2016 go-wordwrap (https://github.com/mitchellh/go-wordwrap) 2 | // Modified work Copyright (c) 2022 spreak maintainers 3 | // 4 | // Licensed under the MIT License. See LICENSE in the project root for license information. 5 | 6 | package po 7 | 8 | import ( 9 | "strings" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestWrapString(t *testing.T) { 16 | cases := []struct { 17 | Input, Output string 18 | Lim int 19 | }{ 20 | // A simple word passes through. 21 | { 22 | "foo", 23 | "foo", 24 | 4, 25 | }, 26 | // A single word that is too long passes through. 27 | // We do not break words. 28 | { 29 | "foobarbaz", 30 | "foobarbaz", 31 | 4, 32 | }, 33 | // Lines are broken at whitespace. 34 | { 35 | "foo bar baz", 36 | "foo\nbar\nbaz", 37 | 4, 38 | }, 39 | // Lines are broken at whitespace, even if words 40 | // are too long. We do not break words. 41 | { 42 | "foo bars bazzes", 43 | "foo\nbars\nbazzes", 44 | 4, 45 | }, 46 | // A word that would run beyond the width is wrapped. 47 | { 48 | "fo sop", 49 | "fo\nsop", 50 | 4, 51 | }, 52 | // Do not break on non-breaking space. 53 | { 54 | "foo bar\u00A0baz", 55 | "foo\nbar\u00A0baz", 56 | 10, 57 | }, 58 | // Whitespace that trails a line and fits the width 59 | // passes through, as does whitespace prefixing an 60 | // explicit line break. A tab counts as one character. 61 | { 62 | "foo\nb\t r\n baz", 63 | "foo\nb\t r\n baz", 64 | 4, 65 | }, 66 | // Trailing whitespace is removed if it doesn't fit the width. 67 | // Runs of whitespace on which a line is broken are removed. 68 | { 69 | "foo \nb ar ", 70 | "foo\nb\nar", 71 | 4, 72 | }, 73 | // An explicit line break at the end of the input is preserved. 74 | { 75 | "foo bar baz\n", 76 | "foo\nbar\nbaz\n", 77 | 4, 78 | }, 79 | // Explicit break are always preserved. 80 | { 81 | "\nfoo bar\n\n\nbaz\n", 82 | "\nfoo\nbar\n\n\nbaz\n", 83 | 4, 84 | }, 85 | // Complete example: 86 | { 87 | " This is a list: \n\n\t* foo\n\t* bar\n\n\n\t* baz \nBAM ", 88 | " This\nis a\nlist:\n\n\t* foo\n\t* bar\n\n\n\t* baz\nBAM", 89 | 6, 90 | }, 91 | // Multi-byte characters 92 | { 93 | "\u2584 \u2584 \u2584 \u2584", 94 | "\u2584 \u2584\n\u2584 \u2584", 95 | 4, 96 | }, 97 | } 98 | 99 | for _, tc := range cases { 100 | actual := strings.Join(wrapString(tc.Input, tc.Lim), "\n") 101 | assert.Equal(t, tc.Output, actual) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /catalog/cldrplural/builtin_test.go: -------------------------------------------------------------------------------- 1 | package cldrplural 2 | 3 | import ( 4 | "reflect" 5 | "runtime" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "golang.org/x/text/language" 12 | ) 13 | 14 | // Returns the name of a function for a function type. 15 | func getFuncName(i any) string { 16 | // github.com/vorlif/spreak/catalog/poplural.forLanguage.newFormCsSk.func28 17 | funcName := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() 18 | if idx := strings.LastIndex(funcName, "."); idx >= 0 { 19 | // github.com/vorlif/spreak/catalog/poplural.forLanguage.newFormCsSk 20 | return funcName[:idx] 21 | } 22 | return funcName 23 | } 24 | 25 | func TestForLanguage(t *testing.T) { 26 | t.Run("if the language is not known a fallback is used", func(t *testing.T) { 27 | unknownLang := language.MustParse("x-art") 28 | forms, found := ForLanguage(unknownLang) 29 | require.False(t, found) 30 | require.NotNil(t, forms) 31 | 32 | fallbackForm := getBuiltInForLanguage(fallbackLanguage) 33 | require.NotNil(t, fallbackForm) 34 | assert.Equal(t, getFuncName(fallbackForm), getFuncName(forms)) 35 | }) 36 | 37 | t.Run("if the language has a region the base is used as fallback", func(t *testing.T) { 38 | ar := language.MustParse("ar") 39 | arForms, arFound := ForLanguage(ar) 40 | require.True(t, arFound) 41 | require.NotNil(t, arForms) 42 | 43 | arDe := language.MustParse("ar-DE") 44 | deForms, deFound := ForLanguage(arDe) 45 | require.True(t, deFound) 46 | require.NotNil(t, deForms) 47 | 48 | assert.Equal(t, getFuncName(arForms), getFuncName(deForms)) 49 | }) 50 | 51 | t.Run("returns the correct value", func(t *testing.T) { 52 | tests := []string{"de", "en", "dz", "ar", "uk", "zh", "lag"} 53 | for _, tt := range tests { 54 | t.Run(tt, func(t *testing.T) { 55 | lang := language.MustParse(tt) 56 | got, gotFound := ForLanguage(lang) 57 | forms := getBuiltInForLanguage(lang.String()) 58 | assert.Equalf(t, getFuncName(forms), getFuncName(got), "ForLanguage(%v)", lang) 59 | assert.True(t, gotFound, "ForLanguage(%v)", lang) 60 | }) 61 | } 62 | }) 63 | 64 | t.Run("uses backward compatibility", func(t *testing.T) { 65 | edgeTests := []struct { 66 | langName string 67 | wantLang string 68 | }{ 69 | {"nl-BE", "nl"}, 70 | {"zh-Hant", "zh"}, 71 | {"de-AT", "de"}, 72 | } 73 | for _, tt := range edgeTests { 74 | t.Run(tt.langName, func(t *testing.T) { 75 | lang := language.MustParse(tt.langName) 76 | got, gotFound := ForLanguage(lang) 77 | forms := getBuiltInForLanguage(lang.String()) 78 | assert.Equalf(t, getFuncName(forms), getFuncName(got), "ForLanguage(%v)", lang) 79 | assert.True(t, gotFound, "ForLanguage(%v)", lang) 80 | }) 81 | } 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /examples/features/printer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "golang.org/x/text/language" 8 | 9 | "github.com/vorlif/spreak" 10 | "github.com/vorlif/spreak/localize" 11 | ) 12 | 13 | const ( 14 | Domain = "printer" 15 | ) 16 | 17 | func main() { 18 | bundle, err := spreak.NewBundle( 19 | spreak.WithSourceLanguage(language.English), 20 | spreak.WithDefaultDomain(Domain), 21 | spreak.WithDomainPath(Domain, "../../locale"), 22 | spreak.WithLanguage(language.German, language.Spanish, language.French), 23 | spreak.WithPrinter(NewPrinter()), 24 | ) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | t := spreak.NewLocalizer(bundle, language.German) 30 | 31 | var msg localize.MsgID 32 | 33 | // TRANSLATORS: %{name}s is the name of a person 34 | msg = "My name is %{name}s and I am %{age}d years old" 35 | 36 | fmt.Println(t.Getf(msg, "name", "Bob", "age", 8)) 37 | // Output: Mein Name ist Bob und ich bin 8 Jahre alt 38 | } 39 | 40 | // A printer creates a function for each language, which is responsible for embedding the variables in the string. 41 | // Like fmt.Sprintf(string, ...variables) 42 | type myPrinter struct{} 43 | 44 | var _ spreak.Printer = (*myPrinter)(nil) 45 | 46 | func NewPrinter() spreak.Printer { 47 | return &myPrinter{} 48 | } 49 | 50 | func (m myPrinter) GetPrintFunc(lang language.Tag) spreak.PrintFunc { 51 | return func(str string, vars ...any) string { 52 | f, p := parse(str, vars...) 53 | return fmt.Sprintf(f, p...) 54 | } 55 | } 56 | 57 | // Information for which languages PrintFuncs probably have to be created. 58 | func (myPrinter) Init(languages []language.Tag) {} 59 | 60 | var re = regexp.MustCompile("%{(\\w+)}[.\\d]*[xsvTtbcdoqXUeEfFgGp]") 61 | 62 | func parse(format string, vars ...any) (string, []any) { 63 | params := make(map[string]any, len(vars)/2) 64 | for i := 0; i < len(vars); i++ { 65 | key := fmt.Sprintf("%v", vars[i]) 66 | if i+1 < len(vars) { 67 | params[key] = vars[i+1] 68 | i++ 69 | } else { 70 | params[key] = key 71 | } 72 | } 73 | 74 | f, n := reformat(format) 75 | p := make([]any, len(n)) 76 | for i, v := range n { 77 | p[i] = params[v] 78 | } 79 | return f, p 80 | } 81 | 82 | // The following code was copied from https://github.com/chonla/format 83 | 84 | func reformat(f string) (string, []string) { 85 | i := re.FindAllStringSubmatchIndex(f, -1) 86 | 87 | var ord []string 88 | pair := []int{0} 89 | for _, v := range i { 90 | ord = append(ord, f[v[2]:v[3]]) 91 | pair = append(pair, v[2]-1) 92 | pair = append(pair, v[3]+1) 93 | } 94 | pair = append(pair, len(f)) 95 | plen := len(pair) 96 | 97 | out := "" 98 | for n := 0; n < plen; n += 2 { 99 | out += f[pair[n]:pair[n+1]] 100 | } 101 | 102 | return out, ord 103 | } 104 | -------------------------------------------------------------------------------- /catalog/po/message_test.go: -------------------------------------------------------------------------------- 1 | package po 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestMessage_AddReference(t *testing.T) { 11 | m := NewMessage() 12 | require.NotNil(t, m) 13 | 14 | t.Run("references can be added", func(t *testing.T) { 15 | assert.Empty(t, m.Comment.References) 16 | ref := &Reference{Path: "path/to/file"} 17 | m.AddReference(ref) 18 | assert.Len(t, m.Comment.References, 1) 19 | 20 | ref2 := &Reference{Path: "path/to/other/file"} 21 | m.AddReference(ref2) 22 | assert.Len(t, m.Comment.References, 2) 23 | }) 24 | 25 | t.Run("References can be added without initialization", func(t *testing.T) { 26 | m.Comment = nil 27 | 28 | ref := &Reference{Path: "path/to/file"} 29 | m.AddReference(ref) 30 | assert.Len(t, m.Comment.References, 1) 31 | }) 32 | } 33 | 34 | func TestMessage_Merge(t *testing.T) { 35 | t.Run("add message", func(t *testing.T) { 36 | msg := NewMessage() 37 | msg.AddReference(&Reference{Path: "b"}) 38 | o := NewMessage() 39 | o.AddReference(&Reference{Path: "a"}) 40 | o.Comment.AddFlag("flag-a") 41 | 42 | msg.Merge(o) 43 | assert.Len(t, msg.Comment.References, 2) 44 | assert.Len(t, msg.Comment.Flags, 1) 45 | }) 46 | 47 | t.Run("nil message", func(t *testing.T) { 48 | f := func() { 49 | msg := NewMessage() 50 | msg.Merge(nil) 51 | } 52 | assert.NotPanics(t, f) 53 | }) 54 | 55 | t.Run("updates plural id", func(t *testing.T) { 56 | msg := NewMessage() 57 | msg.ID = "test" 58 | 59 | other := NewMessage() 60 | other.ID = "test" 61 | other.IDPlural = "test_plural" 62 | 63 | msg.Merge(other) 64 | assert.Equal(t, other.IDPlural, msg.IDPlural) 65 | }) 66 | 67 | t.Run("create comments struct", func(t *testing.T) { 68 | msg := NewMessage() 69 | msg.Comment = nil 70 | assert.Nil(t, msg.Comment) 71 | msg.Merge(NewMessage()) 72 | assert.NotNil(t, msg.Comment) 73 | }) 74 | } 75 | 76 | func TestMessages_Add(t *testing.T) { 77 | t.Run("creates context map", func(t *testing.T) { 78 | ctx := "context" 79 | messages := make(Messages) 80 | assert.NotContains(t, messages, ctx) 81 | 82 | msg := NewMessage() 83 | msg.Context = ctx 84 | msg.ID = "test" 85 | messages.Add(msg) 86 | 87 | if assert.Contains(t, messages, ctx) { 88 | assert.Contains(t, messages[ctx], msg.ID) 89 | assert.Equal(t, messages[ctx][msg.ID], msg) 90 | } 91 | }) 92 | 93 | t.Run("update existing", func(t *testing.T) { 94 | messages := make(Messages) 95 | msg := NewMessage() 96 | msg.ID = "test" 97 | 98 | messages.Add(msg) 99 | 100 | other := NewMessage() 101 | other.ID = msg.ID 102 | messages.Add(other) 103 | 104 | assert.Equal(t, messages[msg.Context][msg.ID], msg) 105 | assert.Len(t, messages, 1) 106 | assert.Len(t, messages[msg.Context], 1) 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /catalog/poplural/builtin_test.go: -------------------------------------------------------------------------------- 1 | package poplural 2 | 3 | import ( 4 | "reflect" 5 | "runtime" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "golang.org/x/text/language" 12 | ) 13 | 14 | // Returns the name of a function for a function type. 15 | func getFuncName(i any) string { 16 | // github.com/vorlif/spreak/catalog/poplural.forLanguage.newFormCsSk.func28 17 | funcName := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() 18 | if idx := strings.LastIndex(funcName, "."); idx >= 0 { 19 | // github.com/vorlif/spreak/catalog/poplural.forLanguage.newFormCsSk 20 | return funcName[:idx] 21 | } 22 | return funcName 23 | } 24 | 25 | func TestForLanguage(t *testing.T) { 26 | t.Run("if the language is not known a fallback is used", func(t *testing.T) { 27 | unknownLang := language.MustParse("art") 28 | rule, found := ForLanguage(unknownLang) 29 | require.False(t, found) 30 | require.NotNil(t, rule) 31 | 32 | fallbackRule := getBuiltInForLanguage(fallbackLanguage) 33 | require.NotNil(t, fallbackRule) 34 | assert.Equal(t, getFuncName(fallbackRule.Evaluate), getFuncName(rule)) 35 | }) 36 | 37 | t.Run("if the language has a region the base is used as fallback", func(t *testing.T) { 38 | ar := language.MustParse("ar") 39 | arRule, arFound := ForLanguage(ar) 40 | require.True(t, arFound) 41 | require.NotNil(t, arRule) 42 | 43 | arDe := language.MustParse("ar-DE") 44 | deRule, deFound := ForLanguage(arDe) 45 | require.True(t, deFound) 46 | require.NotNil(t, deRule) 47 | 48 | assert.Equal(t, getFuncName(arRule), getFuncName(deRule)) 49 | }) 50 | } 51 | 52 | func Test_pluralRuleForLanguage(t *testing.T) { 53 | tests := []string{"de", "en", "dz", "ar", "uk", "zh", "lag"} 54 | for _, tt := range tests { 55 | t.Run(tt, func(t *testing.T) { 56 | lang := language.MustParse(tt) 57 | got, gotFound := pluralRuleForLanguage(lang) 58 | forms := getBuiltInForLanguage(lang.String()) 59 | assert.Equal(t, forms.NPlurals, got.NPlurals, "pluralRuleForLanguage(%v).NPlurals", lang) 60 | assert.Equal(t, getFuncName(forms.FormFunc), getFuncName(got.FormFunc), "pluralRuleForLanguage(%v).FormFunc", lang) 61 | assert.True(t, gotFound, "pluralRuleForLanguage(%v)", lang) 62 | }) 63 | } 64 | 65 | edgeTests := []struct { 66 | langName string 67 | wantLang string 68 | }{ 69 | {"nl-BE", "nl"}, 70 | {"zh-Hant", "zh"}, 71 | {"de-AT", "de"}, 72 | } 73 | for _, tt := range edgeTests { 74 | t.Run(tt.langName, func(t *testing.T) { 75 | lang := language.MustParse(tt.langName) 76 | got, gotFound := pluralRuleForLanguage(lang) 77 | forms := getBuiltInForLanguage(tt.wantLang) 78 | assert.Equal(t, forms.NPlurals, got.NPlurals, "pluralRuleForLanguage(%v).NPlurals", lang) 79 | assert.Equal(t, getFuncName(forms.FormFunc), getFuncName(got.FormFunc), "pluralRuleForLanguage(%v).FormFunc", lang) 80 | assert.True(t, gotFound, "pluralRuleForLanguage(%v)", lang) 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /catalog/cldrplural/generator/json.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "strings" 7 | "unicode" 8 | 9 | "github.com/vorlif/spreak/catalog/cldrplural" 10 | ) 11 | 12 | type dataSet struct { 13 | Languages []string 14 | Rules []*ruleData 15 | } 16 | 17 | type ruleData struct { 18 | Raw string 19 | Category string 20 | } 21 | 22 | func (d *dataSet) Categories() []string { 23 | categories := make([]string, len(d.Rules)) 24 | for i, r := range d.Rules { 25 | categories[i] = r.Category 26 | } 27 | return categories 28 | } 29 | 30 | func (d *dataSet) Name() string { 31 | var b strings.Builder 32 | for _, lang := range d.Languages { 33 | r := []rune(lang) 34 | r[0] = unicode.ToUpper(r[0]) 35 | lang = string(r) 36 | lang = strings.ReplaceAll(lang, " ", "") 37 | lang = strings.ReplaceAll(lang, "-", "_") 38 | b.WriteString(lang) 39 | } 40 | return b.String() 41 | } 42 | 43 | func (rd *ruleData) WithoutExamples() string { 44 | rawRule := rd.Raw[:strings.Index(rd.Raw, "@")] 45 | return strings.TrimSpace(rawRule) 46 | } 47 | 48 | type ruleJSON struct { 49 | Zero string `json:"pluralRule-count-zero"` 50 | One string `json:"pluralRule-count-one"` 51 | Two string `json:"pluralRule-count-two"` 52 | Few string `json:"pluralRule-count-few"` 53 | Many string `json:"pluralRule-count-many"` 54 | Other string `json:"pluralRule-count-other"` 55 | } 56 | 57 | func (j *ruleJSON) ToData() []*ruleData { 58 | data := make([]*ruleData, 0, 3) 59 | if j.Zero != "" { 60 | data = append(data, &ruleData{Raw: j.Zero, Category: cldrplural.CategoryNames[cldrplural.Zero]}) 61 | } 62 | if j.One != "" { 63 | data = append(data, &ruleData{Raw: j.One, Category: cldrplural.CategoryNames[cldrplural.One]}) 64 | } 65 | if j.Two != "" { 66 | data = append(data, &ruleData{Raw: j.Two, Category: cldrplural.CategoryNames[cldrplural.Two]}) 67 | } 68 | if j.Few != "" { 69 | data = append(data, &ruleData{Raw: j.Few, Category: cldrplural.CategoryNames[cldrplural.Few]}) 70 | } 71 | if j.Many != "" { 72 | data = append(data, &ruleData{Raw: j.Many, Category: cldrplural.CategoryNames[cldrplural.Many]}) 73 | } 74 | if j.Other != "" { 75 | data = append(data, &ruleData{Raw: j.Other, Category: cldrplural.CategoryNames[cldrplural.Other]}) 76 | } 77 | return data 78 | } 79 | 80 | func (j *ruleJSON) hash() string { 81 | h := sha256.New() 82 | h.Write([]byte(j.Zero)) 83 | h.Write([]byte(j.One)) 84 | h.Write([]byte(j.Two)) 85 | h.Write([]byte(j.Few)) 86 | h.Write([]byte(j.Many)) 87 | h.Write([]byte(j.Other)) 88 | return hex.EncodeToString(h.Sum(nil)) 89 | } 90 | 91 | type pluralsFile struct { 92 | Supplemental struct { 93 | Version struct { 94 | UnicodeVersion string `json:"_unicodeVersion"` 95 | CldrVersion string `json:"_cldrVersion"` 96 | } `json:"version"` 97 | PluralsTypeCardinal map[string]ruleJSON `json:"plurals-type-cardinal"` 98 | } `json:"supplemental"` 99 | } 100 | -------------------------------------------------------------------------------- /catalog/cldrplural/ast/scanner.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "io" 5 | "regexp" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | type Token int 11 | 12 | const ( 13 | eof Token = iota 14 | unknown 15 | Operand // n, i, v, w, t, f 16 | Value // \d+ 17 | Equal // = 18 | NotEqual // != 19 | ValueRange // value..value 20 | RangeList // (range | value) (',' range_list)* 21 | And // 'and' 22 | Or // 'or' 23 | Remainder // % 24 | sample // '@integer', '@decimal' 25 | sampleValue // 1, 2, 100.50, etc. 26 | sampleRange // sampleValue ('~' sampleValue)? 27 | ) 28 | 29 | var ( 30 | reValue = regexp.MustCompile(`^\d+$`) 31 | reRange = regexp.MustCompile(`^\d+\.\.[1-9]\d*$`) 32 | reSampleValue = regexp.MustCompile(`^((?:0|[1-9]\d*c?\d*)(?:\.\d*)?|(?:0|[1-9]\d*\d*)(?:\.\d*c?\d*)?)$`) 33 | reSampleRange = regexp.MustCompile(`^(?:0|[1-9]\d*)(?:\.\d*)?~(?:0|[1-9]\d*)(?:\.\d*)?$`) 34 | ) 35 | 36 | type scanner struct { 37 | // The tokens of the rule to be scanned 38 | tokens []string 39 | // pos is position of the token at which the scanner is currently located. 40 | pos int 41 | } 42 | 43 | func newScanner(content string) *scanner { 44 | return &scanner{ 45 | tokens: generateTokens(content), 46 | pos: 0, 47 | } 48 | } 49 | 50 | func (s *scanner) Scan() (tok Token, lit string) { 51 | lit, errR := s.read() 52 | if errR != nil { 53 | return eof, "" 54 | } 55 | 56 | switch strings.ToLower(lit) { 57 | case "": 58 | return eof, "" 59 | case "n", "i", "v", "w", "f", "t", "c", "e": 60 | return Operand, lit 61 | case "=": 62 | return Equal, lit 63 | case "%", "mod": 64 | return Remainder, "%" 65 | case "!=": 66 | return NotEqual, "!=" 67 | case "@integer", "@decimal": 68 | return sample, lit 69 | case "and": 70 | return And, lit 71 | case "or": 72 | return Or, lit 73 | case "…", "...", ".": 74 | return s.Scan() 75 | } 76 | 77 | if nextLit, err := s.read(); nextLit == ".." || nextLit == "~" { 78 | lit += nextLit 79 | nextLit, err = s.read() 80 | if err != nil { 81 | return unknown, lit 82 | } 83 | lit += nextLit 84 | } else if err == nil { 85 | s.unread() 86 | } 87 | 88 | if reValue.MatchString(lit) { 89 | return Value, lit 90 | } else if reSampleValue.MatchString(lit) { 91 | return sampleValue, lit 92 | } else if reRange.MatchString(lit) { 93 | return ValueRange, lit 94 | } else if reSampleRange.MatchString(lit) { 95 | return sampleRange, lit 96 | } 97 | 98 | return unknown, lit 99 | } 100 | 101 | func (s *scanner) read() (string, error) { 102 | if s.pos >= len(s.tokens) { 103 | return "", io.EOF 104 | } 105 | 106 | tok := s.tokens[s.pos] 107 | s.pos++ 108 | return tok, nil 109 | } 110 | 111 | func (s *scanner) unread() { 112 | if s.pos > 0 { 113 | s.pos-- 114 | } 115 | } 116 | 117 | func generateTokens(content string) []string { 118 | return strings.FieldsFunc(content, func(r rune) bool { 119 | return unicode.IsSpace(r) || r == ',' 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /catalog/po_decoder_test.go: -------------------------------------------------------------------------------- 1 | package catalog 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "golang.org/x/text/language" 9 | ) 10 | 11 | const decodePoInvalidPluralForm = ` 12 | msgid "" 13 | msgstr "" 14 | "Plural-Forms: invalid-pluralform\n" 15 | ` 16 | 17 | const decodeTestData = ` 18 | msgid "id" 19 | msgstr "ID" 20 | 21 | msgid "empty translation" 22 | msgstr "" 23 | 24 | msgid "%d day" 25 | msgid_plural "%d days" 26 | msgstr[0] "%d Tag" 27 | msgstr[1] "%d Tage" 28 | 29 | msgid "Special\t \"chars\"" 30 | msgstr "Sonder\t \"zeichen\"" 31 | 32 | #, fuzzy 33 | msgid "Unknown" 34 | msgstr "Fuzzy translation" 35 | ` 36 | 37 | func TestPoDecoder(t *testing.T) { 38 | domain := "my-domain" 39 | lang := language.German 40 | 41 | dec := NewPoDecoder() 42 | cat, err := dec.Decode(lang, domain, []byte{}) 43 | assert.Error(t, err) 44 | assert.Nil(t, cat) 45 | 46 | cat, err = dec.Decode(lang, domain, []byte(decodeTestData)) 47 | assert.NoError(t, err) 48 | assert.NotNil(t, cat) 49 | assert.Equal(t, lang, cat.Language()) 50 | 51 | translation, err := cat.Lookup("", "id") 52 | assert.NoError(t, err) 53 | assert.Equal(t, "ID", translation) 54 | 55 | translation, err = cat.Lookup("", "xyz") 56 | assert.Error(t, err) 57 | assert.Equal(t, "xyz", translation) 58 | 59 | translation, err = cat.Lookup("", "Unknown") 60 | assert.Error(t, err) 61 | assert.Equal(t, "Unknown", translation) 62 | 63 | cat, err = dec.Decode(lang, domain, []byte(decodePoInvalidPluralForm+decodeTestData)) 64 | assert.Error(t, err) 65 | assert.Nil(t, cat) 66 | 67 | t.Run("test special chars", func(t *testing.T) { 68 | cat, err = dec.Decode(lang, domain, []byte(decodeTestData)) 69 | assert.NoError(t, err) 70 | require.NotNil(t, cat) 71 | 72 | tr, err := cat.Lookup("", "Special \"chars\"") 73 | assert.NoError(t, err) 74 | assert.Equal(t, "Sonder\t\t\"zeichen\"", tr) 75 | }) 76 | } 77 | 78 | func TestMoDecoder(t *testing.T) { 79 | dec := NewMoDecoder() 80 | decode, err := dec.Decode(language.German, "", []byte{}) 81 | assert.Error(t, err) 82 | assert.Nil(t, decode) 83 | } 84 | 85 | func TestNewPoCLDRDecoder(t *testing.T) { 86 | dec := NewPoCLDRDecoder() 87 | 88 | cat, err := dec.Decode(language.German, "", []byte(decodeTestData)) 89 | assert.NoError(t, err) 90 | require.NotNil(t, cat) 91 | 92 | translation, errT := cat.LookupPlural("", "%d day", "1.2") 93 | assert.NoError(t, errT) 94 | assert.Equal(t, "%d Tage", translation) 95 | } 96 | 97 | func TestCLDRHeader(t *testing.T) { 98 | header := ` 99 | msgid "" 100 | msgstr "" 101 | "Plural-Forms: invalid-pluralform\n" 102 | "X-spreak-use-CLDR: true\n" 103 | ` 104 | 105 | dec := NewPoDecoder() 106 | cat, err := dec.Decode(language.German, "", []byte(header+decodeTestData)) 107 | assert.NoError(t, err) 108 | require.NotNil(t, cat) 109 | translation, errT := cat.LookupPlural("", "%d day", "1.2") 110 | assert.NoError(t, errT) 111 | assert.Equal(t, "%d Tage", translation) 112 | } 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Florian Vogt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | Translations in `humanize/locale` 25 | 26 | Original: 27 | https://github.com/django/django 28 | 29 | License: 30 | https://raw.githubusercontent.com/django/django/1dae4ac1778f04805c0ed62c8debb13b281ba02b/LICENSE 31 | 32 | Copyright (c) Django Software Foundation and individual contributors. 33 | All rights reserved. 34 | 35 | Redistribution and use in source and binary forms, with or without modification, 36 | are permitted provided that the following conditions are met: 37 | 38 | 1. Redistributions of source code must retain the above copyright notice, 39 | this list of conditions and the following disclaimer. 40 | 41 | 2. Redistributions in binary form must reproduce the above copyright 42 | notice, this list of conditions and the following disclaimer in the 43 | documentation and/or other materials provided with the distribution. 44 | 45 | 3. Neither the name of Django nor the names of its contributors may be used 46 | to endorse or promote products derived from this software without 47 | specific prior written permission. 48 | 49 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 50 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 51 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 52 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 53 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 54 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 55 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 56 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 57 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 58 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 59 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package spreak provides a simple translation facility based on the concepts of gettext. 2 | // 3 | // # Fundamentals 4 | // 5 | // Domain: A message domain is a set of translatable messages. 6 | // Usually, every software package has its own message domain. 7 | // The domain name is used to determine the message catalog where the translation is looked up. 8 | // 9 | // Default domain: The default domain is used if a domain is not explicitly specified for a requested translation. 10 | // If no default domain is specified, the default domain of the bundle is used. 11 | // If this was not specified either, the domain is NoDomain (an empty string). 12 | // 13 | // Context: Context can be added to strings to be translated. 14 | // A context dependent translation lookup is when a translation for a given string is searched, 15 | // that is limited to a given context. The translation for the same string in a different context can be different. 16 | // The different translations of the same string in different contexts can be stored in the same MO file, 17 | // and can be edited by the translator in the same PO file. 18 | // The Context string is visible in the PO file to the translator. 19 | // You should try to make it somehow canonical and never changing. 20 | // Because every time you change an Context, the translator will have to review the translation of msgid. 21 | // 22 | // # Plurals 23 | // 24 | // For JSON files only the CLDR plural rules are supported. 25 | // For po and mo files both gettext plural forms and CLDR plural rules are supported. 26 | // The CLDR rules provides better support when floating point numbers are used. 27 | // When using the CLDR plural rules with po files, a notation increasing from "Zero" to "Other" should be used. 28 | // For example, if the used language supports "Zero", "Few" and "Other", Zero should be notated as entry 0, Few as entry 1 and Other as entry 2. 29 | // It is also recommended to define a gettext compatible plural rule. 30 | // On the website https://php-gettext.github.io/Languages/ you can find a list of gettext plural rules which are compatible to the CLDR plural rules. 31 | // 32 | // To use the CLDR rules in po/mo files you can either add a header "X-spreak-use-CLDR: true" or create a decoder with 33 | // catalog.NewPoCLDRDecoder() / catalog.NewMoCLDRDecoder(). 34 | // 35 | // For Polish with One, Few and Other, the structure of a Po file according to this convention could look like this: 36 | // 37 | // msgid "" 38 | // msgstr "" 39 | // "Plural-Forms: n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : 2;\n" 40 | // "X-spreak-use-CLDR: true\n" 41 | // 42 | // msgid "id" 43 | // msgid_plural "plural id" 44 | // msgstr[0] "Translation with the plural form One" 45 | // msgstr[1] "Translation with the plural form Few" 46 | // msgstr[2] "Translation with the plural form Other" 47 | // 48 | // If floating point numbers are used, it is recommended to pass them formatted as strings as they will be displayed later. 49 | // For example, if the number n is to be displayed with two numbers after the decimal point, it should be formatted with fmt.Sprintf("%.2f", n). 50 | package spreak 51 | -------------------------------------------------------------------------------- /catalog/po_test.go: -------------------------------------------------------------------------------- 1 | package catalog 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "golang.org/x/text/language" 11 | ) 12 | 13 | var poFileDomainA = filepath.FromSlash("../testdata/translation-test/de/LC_MESSAGES/a.po") 14 | 15 | func getPoCatalogForDomain(t *testing.T, domain string) Catalog { 16 | data, err := os.ReadFile(poFileDomainA) 17 | require.NoError(t, err) 18 | require.NotNil(t, data) 19 | 20 | cat, errC := NewPoDecoder().Decode(language.German, domain, data) 21 | require.NoError(t, errC) 22 | require.NotNil(t, cat) 23 | return cat 24 | } 25 | 26 | func TestCatalog_SimplePublicFunctions(t *testing.T) { 27 | cat := getPoCatalogForDomain(t, "a") 28 | assert.Equal(t, language.German, cat.Language()) 29 | 30 | translation, err := cat.Lookup("", "id") 31 | if assert.NoError(t, err) { 32 | assert.Equal(t, "ID", translation) 33 | } 34 | 35 | translation, err = cat.LookupPlural("", "%d day", 1) 36 | if assert.NoError(t, err) { 37 | assert.Equal(t, "%d Tag", translation) 38 | } 39 | 40 | translation, err = cat.LookupPlural("", "%d car", 10) 41 | if assert.Error(t, err) { 42 | assert.Equal(t, "%d car", translation) 43 | } 44 | 45 | translation, err = cat.Lookup("context", "Test with context") 46 | if assert.NoError(t, err) { 47 | assert.Equal(t, "Test mit Context", translation) 48 | } 49 | 50 | translation, err = cat.LookupPlural("other", "Test with context", 5) 51 | if assert.Error(t, err) { 52 | assert.Equal(t, "Test with context", translation) 53 | } 54 | 55 | translation, err = cat.LookupPlural("context", "%d result", 1) 56 | if assert.NoError(t, err) { 57 | assert.Equal(t, "%d Ergebniss", translation) 58 | } 59 | 60 | translation, err = cat.LookupPlural("context", "%d result", 10) 61 | if assert.NoError(t, err) { 62 | assert.Equal(t, "%d Ergebnisse", translation) 63 | } 64 | 65 | translation, err = cat.LookupPlural("other", "%d result", 1) 66 | if assert.Error(t, err) { 67 | assert.Equal(t, "%d result", translation) 68 | } 69 | 70 | translation, err = cat.LookupPlural("other", "%d result", 10) 71 | if assert.Error(t, err) { 72 | assert.Equal(t, "%d result", translation) 73 | } 74 | } 75 | 76 | func TestGetCLDRPluralFunction(t *testing.T) { 77 | pf := getCLDRPluralFunction(language.MustParse("kw")) 78 | 79 | tests := []struct { 80 | input int 81 | output int 82 | }{ 83 | {0, 0}, 84 | {1, 1}, 85 | {22, 2}, 86 | {143, 3}, 87 | {161, 4}, 88 | {5, 5}, 89 | {1004, 5}, 90 | } 91 | 92 | for _, test := range tests { 93 | form, err := pf(test.input) 94 | assert.NoError(t, err) 95 | assert.Equal(t, test.output, form) 96 | } 97 | } 98 | 99 | func TestGettextMessage_Translations(t *testing.T) { 100 | msg := GettextMessage{ 101 | translations: map[int]string{0: "Car", 1: "Cars"}, 102 | } 103 | 104 | cpy := msg.Translations() 105 | assert.Equal(t, msg.translations, cpy) 106 | cpy[0] = "Auto" 107 | cpy[1] = "Autos" 108 | assert.NotEqual(t, msg.translations, cpy) 109 | } 110 | -------------------------------------------------------------------------------- /localize/types.go: -------------------------------------------------------------------------------- 1 | package localize 2 | 3 | // MsgID is an alias type for string which is used by xspreak for extracting strings 4 | // MsgID and Singular are synonymous and both represent the ID to identify the message. 5 | type MsgID = string 6 | 7 | // Singular is an alias type for string which is used by xspreak for extracting strings 8 | // MsgID and Singular are synonymous and both represent the ID to identify the message. 9 | type Singular = string 10 | 11 | // Plural is an alias type for string which is used by xspreak for extracting strings. 12 | type Plural = string 13 | 14 | // Context is an alias type for string which is used by xspreak for extracting strings. 15 | type Context = string 16 | 17 | // Domain is an alias type for string which is used by xspreak for extracting strings. 18 | type Domain = string 19 | 20 | // Key is an alias type for string which is used by xspreak for extracting strings. 21 | // The exported key is not stored as text, only the key is stored. 22 | // Should only be used when messages are identified by keys and not by strings. For example when using JSON. 23 | type Key = string 24 | 25 | // PluralKey is an alias type for string which is used by xspreak for extracting strings. 26 | // It represents a MsgId AND a PluralId which identify a message. 27 | // The exported key is not stored as text, only the key is stored. 28 | // Should only be used when messages are identified by keys and not by strings. For example when using JSON. 29 | type PluralKey = string 30 | 31 | // A Localizable allows access to a message that needs to be translated. 32 | // 33 | // An implementation can be passed directly to a spreak.Localizer or spreak.Locale via 34 | // l.Localize(impl) to obtain a translation. 35 | type Localizable interface { 36 | // GetMsgID specifies the message id (singular) for which the message should be translated. 37 | GetMsgID() string 38 | // GetPluralID specifies the plural for which the message should be translated. 39 | GetPluralID() string 40 | // GetContext specifies the context for which the message should be translated. 41 | GetContext() string 42 | // GetVars is optional, can be used to pass parameters. 43 | GetVars() []any 44 | GetCount() int 45 | // HasDomain specifies whether the domain of Domain() is to be used. 46 | // If false the default domain is used. 47 | HasDomain() bool 48 | // GetDomain specifies the domain for which the message should be translated. 49 | GetDomain() string 50 | } 51 | 52 | // Message is a simple struct representing a message without a domain. 53 | // Can be converted to a translated string by spreak.Localizer or spreak.Locale. 54 | type Message struct { 55 | Singular Singular 56 | Plural Plural 57 | Context Context 58 | Vars []any 59 | Count int 60 | } 61 | 62 | var _ Localizable = (*Message)(nil) 63 | 64 | func (m *Message) GetMsgID() string { return m.Singular } 65 | 66 | func (m *Message) GetPluralID() string { return m.Plural } 67 | 68 | func (m *Message) GetContext() string { return m.Context } 69 | 70 | func (m *Message) GetVars() []any { return m.Vars } 71 | 72 | func (m *Message) GetCount() int { return m.Count } 73 | 74 | func (m *Message) HasDomain() bool { return false } 75 | 76 | func (m *Message) GetDomain() string { return "" } 77 | --------------------------------------------------------------------------------