cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / locale / locale.go
locale.go 9.3K
1// Package locale is the table of spreadsheet locales, as Sheets' File >2// Settings > Locale offers them: how numbers, dates and currency are3// typed and shown, and how formulas separate their arguments. It holds4// only the conventions, with no dependencies (no CLDR data): the5// packages that parse and render use them at their edges, while cells,6// formulas and files stay in the canonical form of en-US.7package locale89import (10 "strings"11)1213// DateOrder is the order of day, month and year in a date typed or14// shown with numbers only.15type DateOrder uint81617const (18 MDY DateOrder = iota // 9/26/202619 DMY // 26.09.202620 YMD // 2026-09-2621)2223func (o DateOrder) String() string { return [...]string{"MDY", "DMY", "YMD"}[o] }2425// Locale is one locale's conventions.26type Locale struct {27 Tag string // BCP 47, as written in files and the config: "de-DE"28 Name string // as the picker lists it: "German (Germany)"29 // LCID is Windows' number for the locale, which Excel's format30 // codes name it by: [$€-407], [$-407]d.m.yyyy.31 LCID uint163233 Decimal byte // the decimal separator: '.' or ','34 Group string // the thousands separator shown: ",", ".", a no-break space, an apostrophe3536 Order DateOrder37 DateSep byte // between the numbers of a date: '/', '.' or '-'38 Date string // the Date format's pattern: "dd.mm.yyyy"39 Time string // the Time format's pattern: "hh:mm:ss"4041 Currency string // the currency symbol: "€"42 After bool // the symbol follows the number: "1.234,56 €"43 Space bool // a space between the symbol and the number: "R$ 1.234,56"4445 names *Names // the language's month and day names, see Names46}4748// Canonical is en-US: what cells, formulas and files store, whatever the49// locale shown.50var Canonical = &table[0]5152// nbsp and nnbsp are the no-break spaces locales group digits with, so53// a number never breaks across lines.54const (55 nbsp = " "56 nnbsp = " "57)5859// table lists the locales, en-US first. Date, Time and Currency are the60// formats of Format > Number in each. Dates show day and month without61// leading zeros, as en-US's m/d/yyyy does, so most fit a default column62// (26.9.2026); where a country writes ISO dates they keep theirs.63var table = []Locale{64 {Tag: "en-US", LCID: 0x409, Name: "English (United States)", Decimal: '.', Group: ",", Order: MDY, DateSep: '/', Date: "m/d/yyyy", Time: "h:mm:ss am/pm", Currency: "$"},65 {Tag: "en-GB", LCID: 0x809, Name: "English (United Kingdom)", Decimal: '.', Group: ",", Order: DMY, DateSep: '/', Date: "d/m/yyyy", Time: "hh:mm:ss", Currency: "£"},66 {Tag: "en-CA", LCID: 0x1009, Name: "English (Canada)", Decimal: '.', Group: ",", Order: YMD, DateSep: '-', Date: "yyyy-mm-dd", Time: "h:mm:ss am/pm", Currency: "$"},67 {Tag: "en-AU", LCID: 0xC09, Name: "English (Australia)", Decimal: '.', Group: ",", Order: DMY, DateSep: '/', Date: "d/m/yyyy", Time: "h:mm:ss am/pm", Currency: "$"},68 {Tag: "de-DE", LCID: 0x407, Name: "German (Germany)", Decimal: ',', Group: ".", Order: DMY, DateSep: '.', Date: "d.m.yyyy", Time: "hh:mm:ss", Currency: "€", After: true, Space: true},69 {Tag: "de-CH", LCID: 0x807, Name: "German (Switzerland)", Decimal: '.', Group: "’", Order: DMY, DateSep: '.', Date: "d.m.yyyy", Time: "hh:mm:ss", Currency: "CHF", Space: true},70 {Tag: "fr-FR", LCID: 0x40C, Name: "French (France)", Decimal: ',', Group: nnbsp, Order: DMY, DateSep: '/', Date: "d/m/yyyy", Time: "hh:mm:ss", Currency: "€", After: true, Space: true},71 {Tag: "fr-CA", LCID: 0xC0C, Name: "French (Canada)", Decimal: ',', Group: nbsp, Order: YMD, DateSep: '-', Date: "yyyy-mm-dd", Time: "hh:mm:ss", Currency: "$", After: true, Space: true},72 {Tag: "es-ES", LCID: 0xC0A, Name: "Spanish (Spain)", Decimal: ',', Group: ".", Order: DMY, DateSep: '/', Date: "d/m/yyyy", Time: "h:mm:ss", Currency: "€", After: true, Space: true},73 {Tag: "es-MX", LCID: 0x80A, Name: "Spanish (Mexico)", Decimal: '.', Group: ",", Order: DMY, DateSep: '/', Date: "d/m/yyyy", Time: "hh:mm:ss", Currency: "$"},74 {Tag: "it-IT", LCID: 0x410, Name: "Italian (Italy)", Decimal: ',', Group: ".", Order: DMY, DateSep: '/', Date: "d/m/yyyy", Time: "hh:mm:ss", Currency: "€", After: true, Space: true},75 {Tag: "pt-BR", LCID: 0x416, Name: "Portuguese (Brazil)", Decimal: ',', Group: ".", Order: DMY, DateSep: '/', Date: "d/m/yyyy", Time: "hh:mm:ss", Currency: "R$", Space: true},76 {Tag: "pt-PT", LCID: 0x816, Name: "Portuguese (Portugal)", Decimal: ',', Group: nbsp, Order: DMY, DateSep: '/', Date: "d/m/yyyy", Time: "hh:mm:ss", Currency: "€", After: true, Space: true},77 {Tag: "nl-NL", LCID: 0x413, Name: "Dutch (Netherlands)", Decimal: ',', Group: ".", Order: DMY, DateSep: '-', Date: "d-m-yyyy", Time: "hh:mm:ss", Currency: "€", Space: true},78 {Tag: "sv-SE", LCID: 0x41D, Name: "Swedish (Sweden)", Decimal: ',', Group: nbsp, Order: YMD, DateSep: '-', Date: "yyyy-mm-dd", Time: "hh:mm:ss", Currency: "kr", After: true, Space: true},79 {Tag: "da-DK", LCID: 0x406, Name: "Danish (Denmark)", Decimal: ',', Group: ".", Order: DMY, DateSep: '.', Date: "d.m.yyyy", Time: "hh:mm:ss", Currency: "kr.", After: true, Space: true},80 {Tag: "nb-NO", LCID: 0x414, Name: "Norwegian (Norway)", Decimal: ',', Group: nbsp, Order: DMY, DateSep: '.', Date: "d.m.yyyy", Time: "hh:mm:ss", Currency: "kr", Space: true},81 {Tag: "fi-FI", LCID: 0x40B, Name: "Finnish (Finland)", Decimal: ',', Group: nbsp, Order: DMY, DateSep: '.', Date: "d.m.yyyy", Time: "h:mm:ss", Currency: "€", After: true, Space: true},82 {Tag: "pl-PL", LCID: 0x415, Name: "Polish (Poland)", Decimal: ',', Group: nbsp, Order: DMY, DateSep: '.', Date: "d.m.yyyy", Time: "hh:mm:ss", Currency: "zł", After: true, Space: true},83 {Tag: "cs-CZ", LCID: 0x405, Name: "Czech (Czechia)", Decimal: ',', Group: nbsp, Order: DMY, DateSep: '.', Date: "d.m.yyyy", Time: "h:mm:ss", Currency: "Kč", After: true, Space: true},84 {Tag: "ru-RU", LCID: 0x419, Name: "Russian (Russia)", Decimal: ',', Group: nbsp, Order: DMY, DateSep: '.', Date: "d.m.yyyy", Time: "h:mm:ss", Currency: "₽", After: true, Space: true},85 {Tag: "tr-TR", LCID: 0x41F, Name: "Turkish (Türkiye)", Decimal: ',', Group: ".", Order: DMY, DateSep: '.', Date: "d.m.yyyy", Time: "hh:mm:ss", Currency: "₺"},86 {Tag: "ja-JP", LCID: 0x411, Name: "Japanese (Japan)", Decimal: '.', Group: ",", Order: YMD, DateSep: '/', Date: "yyyy/m/d", Time: "h:mm:ss", Currency: "¥"},87 {Tag: "zh-CN", LCID: 0x804, Name: "Chinese (China)", Decimal: '.', Group: ",", Order: YMD, DateSep: '/', Date: "yyyy/m/d", Time: "h:mm:ss", Currency: "¥"},88}8990// All returns every locale, en-US first, then as the table lists them.91func All() []*Locale {92 out := make([]*Locale, len(table))93 for i := range table {94 out[i] = &table[i]95 }96 return out97}9899// Tags are the tags of every locale, in the table's order.100func Tags() []string {101 out := make([]string, len(table))102 for i := range table {103 out[i] = table[i].Tag104 }105 return out106}107108// Lookup finds a locale by its tag, ignoring case and taking _ for -, so109// "de-DE", "de_de" and "DE-de" are the same. A language alone ("de")110// means its first locale in the table.111func Lookup(tag string) (*Locale, bool) {112 tag = strings.ReplaceAll(strings.TrimSpace(tag), "_", "-")113 if tag == "" {114 return nil, false115 }116 for i := range table {117 if strings.EqualFold(table[i].Tag, tag) {118 return &table[i], true119 }120 }121 if !strings.Contains(tag, "-") {122 for i := range table {123 if lang, _, _ := strings.Cut(table[i].Tag, "-"); strings.EqualFold(lang, tag) {124 return &table[i], true125 }126 }127 }128 return nil, false129}130131// ByLCID finds a locale by its Windows number, as Excel's format codes132// write it.133func ByLCID(id uint16) (*Locale, bool) {134 for i := range table {135 if table[i].LCID == id {136 return &table[i], true137 }138 }139 return nil, false140}141142// FromPOSIX reads a POSIX locale, as LANG and LC_ALL hold it143// ("de_DE.UTF-8", "fr_FR@euro"), returning the tag of the locale in the144// table it names. C, POSIX and locales outside the table give "".145func FromPOSIX(v string) string {146 v, _, _ = strings.Cut(v, ".")147 v, _, _ = strings.Cut(v, "@")148 if v == "C" || v == "POSIX" {149 return ""150 }151 if l, ok := Lookup(v); ok {152 return l.Tag153 }154 return ""155}156157// IsCanonical reports whether l types and shows everything as stored:158// en-US, or nil.159func (l *Locale) IsCanonical() bool { return l == nil || l == Canonical }160161// Or returns l, or the canonical locale when l is nil.162func (l *Locale) Or() *Locale {163 if l == nil {164 return Canonical165 }166 return l167}168169// ArgSep separates a formula's arguments: ; where the decimal separator170// is a comma, as in Sheets, so 1,5 stays a number.171func (l *Locale) ArgSep() byte {172 if l != nil && l.Decimal == ',' {173 return ';'174 }175 return ','176}177178// ColSep separates the values of a row in an array literal: \ where the179// argument separator is ;, which also separates an array's rows.180func (l *Locale) ColSep() byte {181 if l != nil && l.Decimal == ',' {182 return '\\'183 }184 return ','185}186187// IsGroup reports whether r separates thousands in l as typed: its own188// separator, any space where it groups with a space, and ' for ’.189func (l *Locale) IsGroup(r rune) bool {190 if string(r) == l.Group {191 return true192 }193 switch l.Group {194 case nbsp, nnbsp:195 return r == ' ' || r == ' ' || r == ' '196 case "’":197 return r == '\''198 }199 return false200}201202// DateTime is the Date time format's pattern: the date, then the time on203// the 24-hour clock, as Sheets shows it in every locale.204func (l *Locale) DateTime() string {205 t := strings.TrimSpace(strings.Replace(l.Time, "am/pm", "", 1))206 return l.Date + " " + t207}