Skip to content

Latest commit

 

History

History

language

README.md

language

ISO 639-1 two-letter language codes. Code normalizes the language identifiers found in HTTP headers, locales and other standards ("EN", "deu", "ger", "en-US", "zh_Hant") to the canonical lower-case ISO 639-1 code, returns English language names, and reads and writes SQL, JSON and XML. Two lookup functions cover the full ISO 639-3 tables. The date package uses Code as a parsing hint.

import "github.com/domonda/go-types/language"

Code

type Code string //#enum

const Null Code = "" //#exclude

Value writes Null as SQL NULL and Scan reads SQL NULL as Null. The constant is marked //#exclude so that go-enum leaves it out of Enums(). Null is not a valid code: Null.Valid() is false and Null.Validate() returns an error. There is no nullable code type in this package.

Constants

The package declares 184 Code constants, one per ISO 639-1 code, named after the upper-case code with a lower-case value: EN Code = "en", DE Code = "de", ZH Code = "zh", ... Note that language.EL is Greek, unrelated to country.EL.

List them with the go-enum generated methods. Both return the codes in declaration order, which is close to alphabetical by English name:

codes := language.Code("").Enums()       // []language.Code{language.AA, language.AB, language.AF, ...}
names := language.Code("").EnumStrings() // []string{"aa", "ab", "af", ...}

Functions and methods

Identifier Description
ParseCode(str) Canonical code from a code, tag or English language name, see below.
Normalized() The canonical ISO 639-1 code for the input shapes listed below. On failure returns the input unchanged and an error that quotes it.
Valid() True if Normalized() succeeds, so Code("EN").Valid() is true.
Validate() The error of Normalized(), which quotes the input, or nil.
ValidAndNormalized() True only for a canonical lower-case code.
LanguageName() English name from the ISO 639-1 table. Normalizes first; "" if invalid.
String() The code as stored, generated by go-enum.
Enums(), EnumStrings() All 184 codes, generated by go-enum.
ISO6393Name(code) English reference name of an ISO 639-3 code ("deu" gives "German"), "" if unknown.
ISO6393Macrolanguage(code) ISO 639-3 macrolanguage of an individual language ("twi" gives "aka"), "" if none.

Implements: fmt.Stringer, database/sql.Scanner, database/sql/driver.Valuer, encoding/json.Marshaler, encoding/json.Unmarshaler, encoding.TextMarshaler, encoding/xml.Unmarshaler, encoding/xml.UnmarshalerAttr, encoding/xml.MarshalerAttr, types.NormalizableValidator[Code] and JSONSchema() ({"type":"string","pattern":"^[a-z]{2}$"}). ScanString makes it a strfmt.Scannable, which reads a code like Scan. Code has no UnmarshalText, so encoding/json reads the keys of a map[Code]V as given.

Valid and Validate are hand-written and carry a // #custom marker, so go-enum keeps them. They normalize first like those of country.Code and money.Currency. Reading and Value only trim. Call Normalized() for the ISO 639-1 code. String is the one go-enum generates: it returns the stored code.

Normalized input shapes

Normalized(), and so Valid() and LanguageName(), accept the spellings below. Reading and writing only trim, so "eng", "ger" and "en-US" are stored as given:

  • ISO 639-1 codes in any case with surrounding whitespace: "en", "EN", " en ".
  • ISO 639-2/T and ISO 639-3 codes that have an ISO 639-1 equivalent: "eng", "deu", "fra".
  • ISO 639-2/B bibliographic codes: "ger" gives "de", "fre" gives "fr", "chi" gives "zh".
  • BCP-47 tags and POSIX locales, using only the primary subtag: "en-US", "zh-Hant-CN", "sr-Latn", "en_US".

ISO 639-3 codes without an ISO 639-1 equivalent (most of the roughly 7900 codes, like "abq") are rejected.

ParseCode

ParseCode(str) tries the cheapest interpretation first:

  1. A canonical code ("en") is returned as is.
  2. Anything Normalized accepts.
  3. An English language name, matched case-insensitively: "German", "english", "Valencian". Names come from the ISO 639-1 table, including each synonym of entries like "Catalan; Valencian", and from the ISO 639-3 names of languages that have an ISO 639-1 code. The ISO 639-1 names win on conflicts.

On failure it returns the input unchanged and the error of Normalized.

ISO 639-3 lookups

ISO6393Name and ISO6393Macrolanguage read tables generated from the SIL ISO 639-3 data. Both are case-sensitive and expect the canonical lower-case code: ISO6393Name("DEU") is "". A code with several names returns its primary name. A macrolanguage is not a member of itself (ISO6393Macrolanguage("aka") is ""), and retired mappings are not returned.

Serialization

Code follows the rules of Reading and writing values: reading and writing only trim, so "EN" stays "EN", and fmt prints the stored code.

Value SQL Value() JSON (json.Marshal) MarshalText, XML fmt
Null, Code(" ") NULL "" "" as stored
Code("en") "en" "en" "en" en
Code("EN") "EN" "EN" "EN" EN
Code("xx") (invalid) "xx" "xx" "xx" xx
Input Result
Scan(nil) Null
Scan of a string or []byte, JSON string, XML element or attribute trimmed, Null if blank, case kept, without error
Scan of other types error
JSON null field left unchanged
other JSON values error

Gotchas

  • Valid accepts other spellings. Code("EN").Valid() and Code("EN").Validate() accept "EN", "eng" and "en-US", like types.Validate, types.DeepValidate and strfmt scanning. Check ValidAndNormalized to require the canonical code.
  • A code keeps its spelling. Scan, JSON and XML only trim, so "EN" stays "EN" and "en-US" stays "en-US". Call Normalized() for the ISO 639-1 code.
  • Optional languages. Null is SQL NULL but fails Validate, so an empty language field fails types.DeepValidate. Use a *language.Code for optional fields or check for Null before validating.
  • JSON is a string. Null encodes as "", not null. The JSON schema pattern ^[a-z]{2}$ describes a canonical code. MarshalJSON writes the trimmed code, so it can write "EN".
  • English names must match a table entry. ParseCode("Greek") fails, because the names are "Greek, Modern (1453-)" (ISO 639-1) and "Modern Greek (1453-)" (ISO 639-3).
  • Names may list synonyms. language.CA.LanguageName() is "Catalan; Valencian", language.NL.LanguageName() is "Dutch; Flemish".
  • Tags lose detail. "zh-Hant" and "zh-Hans" both normalize to "zh".
  • Date parsing hints. date.Normalize only distinguishes English from everything else, see the date package.

Examples

package main

import (
	"fmt"

	"github.com/domonda/go-types/language"
)

func main() {
	for _, s := range []string{"en", "EN", "deu", "ger", "en-US", "zh_Hant", "German", "Klingon"} {
		c, err := language.ParseCode(s)
		fmt.Println(c, err)
	}

	// Valid, Validate and Normalized accept other spellings
	c := language.Code("EN")
	fmt.Println(c.Valid(), c.Validate(), c.LanguageName())
	norm, err := c.Normalized()
	fmt.Println(norm, err, norm.Validate())

	// Output:
	// en <nil>
	// en <nil>
	// de <nil>
	// de <nil>
	// en <nil>
	// zh <nil>
	// de <nil>
	// Klingon invalid language.Code: "Klingon"
	// true <nil> English
	// en <nil> <nil>
}
package main

import (
	"encoding/json"
	"fmt"

	"github.com/domonda/go-types/language"
)

func main() {
	// Value and MarshalJSON write the trimmed code
	v, _ := language.Code("EN").Value()
	fmt.Println(v)
	j, _ := json.Marshal(language.Code("en-US"))
	fmt.Println(string(j))

	fmt.Println(language.ISO6393Name("deu"), language.ISO6393Macrolanguage("twi"))
	fmt.Println(language.CA.LanguageName())
	fmt.Println(len(language.Code("").Enums()))

	// Output:
	// EN
	// "en-US"
	// German aka
	// Catalan; Valencian
	// 184
}

Data files

File Content Maintained by
code.go Code, its methods and the ISO 639-1 constants; Enums() and EnumStrings() at the end hand; go-enum for Enums and EnumStrings
constants.go codeNames: ISO 639-1 code to English name hand
iso6393.go iso6393To1: ISO 639-2/T, 639-2/B and 639-3 codes to ISO 639-1, and the two lookup functions hand
iso6393names.go iso6393Names: ISO 639-3 code to English name gen-lang-maps
iso6393macro.go iso6393Macro: individual language to macrolanguage gen-lang-maps

Refresh the two generated tables with go generate ./language/... (needs network access), see gen-lang-maps.

Adding a language code

  1. Add the constant to the const block in language/code.go: XX Code = "xx" // Name.
  2. Add "xx": "Name" to codeNames in language/constants.go. TestCode_EnumsMatchCodeNames fails if the two lists differ.
  3. Add the ISO 639-2/T, 639-2/B and 639-3 codes of the language to iso6393To1 in language/iso6393.go, so that Normalized maps them.
  4. Run ./run-go-enum.sh from the repository root to regenerate Enums() and EnumStrings(), then go test ./....
  5. CI runs go-enum -validate and fails if the generated methods are stale.

See Add an enum code for the full steps.

Related