2018-01-21 07:49:17 +01:00
// Copyright (c) 2018 Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
2018-02-03 10:46:14 +01:00
package languages
2018-01-21 07:49:17 +01:00
import (
2019-02-19 08:54:57 +01:00
"encoding/json"
2018-01-25 10:51:02 +01:00
"fmt"
2021-02-17 21:11:54 +01:00
"os"
2019-02-19 08:54:57 +01:00
"path/filepath"
2018-01-25 10:51:02 +01:00
"sort"
2019-02-19 08:54:57 +01:00
"strconv"
2018-01-22 08:30:31 +01:00
"strings"
2019-02-19 08:54:57 +01:00
"gopkg.in/yaml.v2"
)
const (
// for a language (e.g., `fi-FI`) to be supported
// it must have a metadata file named, e.g., `fi-FI.lang.yaml`
metadataFileSuffix = ".lang.yaml"
)
var (
stringsFileSuffixes = [ ] string { "-irc.lang.json" , "-help.lang.json" , "-nickserv.lang.json" , "-hostserv.lang.json" , "-chanserv.lang.json" }
2018-01-21 07:49:17 +01:00
)
2018-02-03 10:46:14 +01:00
// LangData is the data contained in a language file.
type LangData struct {
Name string
Code string
Contributors string
Incomplete bool
}
// Manager manages our languages and provides translation abilities.
type Manager struct {
2019-02-19 08:54:57 +01:00
Languages map [ string ] LangData
2018-01-22 08:30:31 +01:00
translations map [ string ] map [ string ] string
2018-01-22 12:30:28 +01:00
defaultLang string
2018-01-21 07:49:17 +01:00
}
2018-02-03 10:46:14 +01:00
// NewManager returns a new Manager.
2019-02-19 08:54:57 +01:00
func NewManager ( enabled bool , path string , defaultLang string ) ( lm * Manager , err error ) {
lm = & Manager {
Languages : make ( map [ string ] LangData ) ,
2018-01-22 08:30:31 +01:00
translations : make ( map [ string ] map [ string ] string ) ,
2018-01-22 12:30:28 +01:00
defaultLang : defaultLang ,
2018-01-21 07:49:17 +01:00
}
2018-01-22 08:30:31 +01:00
// make fake "en" info
2019-02-19 08:54:57 +01:00
lm . Languages [ "en" ] = LangData {
2018-01-23 07:50:19 +01:00
Code : "en" ,
Name : "English" ,
Contributors : "Oragono contributors and the IRC community" ,
2018-01-22 08:30:31 +01:00
}
2019-02-19 08:54:57 +01:00
if enabled {
err = lm . loadData ( path )
if err == nil {
// successful load, check that defaultLang is sane
_ , ok := lm . Languages [ lm . defaultLang ]
if ! ok {
err = fmt . Errorf ( "Cannot find default language [%s]" , lm . defaultLang )
}
}
} else {
lm . defaultLang = "en"
}
return
}
func ( lm * Manager ) loadData ( path string ) ( err error ) {
2021-02-17 21:11:54 +01:00
files , err := os . ReadDir ( path )
2019-02-19 08:54:57 +01:00
if err != nil {
return
}
// 1. for each language that has a ${langcode}.lang.yaml in the languages path
// 2. load ${langcode}.lang.yaml
// 3. load ${langcode}-irc.lang.json and friends as the translations
for _ , f := range files {
if f . IsDir ( ) {
continue
}
// glob up *.lang.yaml in the directory
name := f . Name ( )
if ! strings . HasSuffix ( name , metadataFileSuffix ) {
continue
}
prefix := strings . TrimSuffix ( name , metadataFileSuffix )
// load, e.g., `zh-CN.lang.yaml`
var data [ ] byte
2021-02-17 21:11:54 +01:00
data , err = os . ReadFile ( filepath . Join ( path , name ) )
2019-02-19 08:54:57 +01:00
if err != nil {
return
}
var langInfo LangData
err = yaml . Unmarshal ( data , & langInfo )
if err != nil {
return err
}
if langInfo . Code == "en" {
return fmt . Errorf ( "Cannot have language file with code 'en' (this is the default language using strings inside the server code). If you're making an English variant, name it with a more specific code" )
}
// check for duplicate languages
_ , exists := lm . Languages [ strings . ToLower ( langInfo . Code ) ]
if exists {
return fmt . Errorf ( "Language code [%s] defined twice" , langInfo . Code )
}
// slurp up all translation files with `prefix` into a single translation map
translations := make ( map [ string ] string )
for _ , translationSuffix := range stringsFileSuffixes {
stringsFilePath := filepath . Join ( path , prefix + translationSuffix )
2021-02-17 21:11:54 +01:00
data , err = os . ReadFile ( stringsFilePath )
2019-02-19 08:54:57 +01:00
if err != nil {
continue // skip missing paths
}
var tlList map [ string ] string
err = json . Unmarshal ( data , & tlList )
if err != nil {
return fmt . Errorf ( "invalid json for translation file %s: %s" , stringsFilePath , err . Error ( ) )
}
2018-01-23 06:06:55 +01:00
2019-02-19 08:54:57 +01:00
for key , value := range tlList {
// because of how crowdin works, this is how we skip untranslated lines
if key == value || strings . TrimSpace ( value ) == "" {
continue
}
translations [ key ] = value
2018-01-23 06:06:55 +01:00
}
}
2019-02-19 08:54:57 +01:00
if len ( translations ) == 0 {
// skip empty translations
continue
}
// sanity check the language definition from the yaml file
if langInfo . Code == "" || langInfo . Name == "" || langInfo . Contributors == "" {
return fmt . Errorf ( "Code, name or contributors is empty in language file [%s]" , name )
}
key := strings . ToLower ( langInfo . Code )
lm . Languages [ key ] = langInfo
lm . translations [ key ] = translations
2018-01-22 08:30:31 +01:00
}
2018-01-21 07:49:17 +01:00
2019-02-19 08:54:57 +01:00
return nil
2018-01-21 07:49:17 +01:00
}
2018-01-22 12:30:28 +01:00
// Default returns the default languages.
2018-02-03 10:46:14 +01:00
func ( lm * Manager ) Default ( ) [ ] string {
2018-01-22 12:30:28 +01:00
return [ ] string { lm . defaultLang }
}
2018-01-22 08:30:31 +01:00
// Count returns how many languages we have.
2018-02-03 10:46:14 +01:00
func ( lm * Manager ) Count ( ) int {
2019-02-19 08:54:57 +01:00
return len ( lm . Languages )
2018-01-22 08:30:31 +01:00
}
2019-07-01 15:21:38 +02:00
// Enabled returns whether translation is enabled.
func ( lm * Manager ) Enabled ( ) bool {
return len ( lm . translations ) != 0
}
2018-01-25 10:51:02 +01:00
// Translators returns the languages we have and the translators.
2018-02-03 10:46:14 +01:00
func ( lm * Manager ) Translators ( ) [ ] string {
2018-01-25 10:51:02 +01:00
var tlist sort . StringSlice
2019-02-19 08:54:57 +01:00
for _ , info := range lm . Languages {
2018-01-25 10:51:02 +01:00
if info . Code == "en" {
continue
}
tlist = append ( tlist , fmt . Sprintf ( "%s (%s): %s" , info . Name , info . Code , info . Contributors ) )
}
2019-03-07 08:31:46 +01:00
tlist . Sort ( )
2018-01-25 10:51:02 +01:00
return tlist
}
2018-01-22 08:30:31 +01:00
// Codes returns the proper language codes for the given casefolded language codes.
2018-02-03 10:46:14 +01:00
func ( lm * Manager ) Codes ( codes [ ] string ) [ ] string {
2018-01-22 08:30:31 +01:00
var newCodes [ ] string
for _ , code := range codes {
2019-02-19 08:54:57 +01:00
info , exists := lm . Languages [ code ]
2018-01-22 08:30:31 +01:00
if exists {
newCodes = append ( newCodes , info . Code )
}
}
if len ( newCodes ) == 0 {
newCodes = [ ] string { "en" }
}
return newCodes
}
2018-01-21 07:49:17 +01:00
// Translate returns the given string, translated into the given language.
2018-02-03 10:46:14 +01:00
func ( lm * Manager ) Translate ( languages [ ] string , originalString string ) string {
2018-01-21 07:49:17 +01:00
// not using any special languages
2018-01-22 08:30:31 +01:00
if len ( languages ) == 0 || languages [ 0 ] == "en" || len ( lm . translations ) == 0 {
2018-01-21 07:49:17 +01:00
return originalString
}
for _ , lang := range languages {
2018-01-22 08:30:31 +01:00
lang = strings . ToLower ( lang )
if lang == "en" {
return originalString
}
translations , exists := lm . translations [ lang ]
2018-01-21 07:49:17 +01:00
if ! exists {
continue
}
2018-01-22 08:30:31 +01:00
newString , exists := translations [ originalString ]
2018-01-21 07:49:17 +01:00
if ! exists {
continue
}
// found a valid translation!
return newString
}
// didn't find any translation
return originalString
}
2019-02-19 08:54:57 +01:00
func ( lm * Manager ) CapValue ( ) string {
2019-03-07 08:31:46 +01:00
langCodes := make ( sort . StringSlice , len ( lm . Languages ) + 1 )
2019-02-19 08:54:57 +01:00
langCodes [ 0 ] = strconv . Itoa ( len ( lm . Languages ) )
i := 1
for _ , info := range lm . Languages {
codeToken := info . Code
if info . Incomplete {
codeToken = "~" + info . Code
}
langCodes [ i ] = codeToken
i += 1
}
2019-03-07 08:31:46 +01:00
langCodes . Sort ( )
2019-02-19 08:54:57 +01:00
return strings . Join ( langCodes , "," )
}