461 lines
13 KiB
Go
461 lines
13 KiB
Go
//go:build ignore
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
"compress/gzip"
|
|
"encoding/xml"
|
|
"flag"
|
|
"fmt"
|
|
"go/format"
|
|
"go/token"
|
|
"log"
|
|
"maps"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/microsoft/typescript-go/internal/collections"
|
|
"github.com/microsoft/typescript-go/internal/json"
|
|
"github.com/microsoft/typescript-go/internal/repo"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
type diagnosticMessage struct {
|
|
Category string `json:"category"`
|
|
Code int `json:"code"`
|
|
ReportsUnnecessary bool `json:"reportsUnnecessary"`
|
|
ReportsDeprecated bool `json:"reportsDeprecated"`
|
|
// spelling error here is [sic] in Strada
|
|
ElidedInCompatibilityPyramid bool `json:"elidedInCompatabilityPyramid"`
|
|
|
|
key string
|
|
}
|
|
|
|
type LCX struct {
|
|
TgtCul string `xml:"TgtCul,attr"`
|
|
RootItems []RootItem `xml:"Item"`
|
|
}
|
|
|
|
type RootItem struct {
|
|
ItemId string `xml:"ItemId,attr"`
|
|
Items []StringTableItem `xml:"Item"`
|
|
}
|
|
|
|
type StringTableItem struct {
|
|
ItemId string `xml:"ItemId,attr"`
|
|
Items []LocalizedItem `xml:"Item"`
|
|
}
|
|
|
|
type LocalizedItem struct {
|
|
ItemId string `xml:"ItemId,attr"`
|
|
Str Str `xml:"Str"`
|
|
}
|
|
|
|
type Str struct {
|
|
Val string `xml:"Val"`
|
|
Tgt *Tgt `xml:"Tgt"`
|
|
}
|
|
|
|
type Tgt struct {
|
|
Val string `xml:"Val"`
|
|
}
|
|
|
|
func main() {
|
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
|
|
diagnosticsOutput := flag.String("diagnostics", "", "path to the output diagnostics_generated.go file")
|
|
locOutput := flag.String("loc", "", "path to the output loc_generated.go file")
|
|
locDir := flag.String("locdir", "", "directory to write locale .json.gz files")
|
|
flag.Parse()
|
|
|
|
if *diagnosticsOutput == "" || *locOutput == "" || *locDir == "" {
|
|
flag.Usage()
|
|
return
|
|
}
|
|
|
|
rawDiagnosticMessages := readRawMessages(filepath.Join(repo.TypeScriptSubmodulePath(), "src", "compiler", "diagnosticMessages.json"))
|
|
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
panic("could not get current filename")
|
|
}
|
|
filename = filepath.FromSlash(filename) // runtime.Caller always returns forward slashes; https://go.dev/issues/3335, https://go.dev/cl/603275
|
|
|
|
rawExtraMessages := readRawMessages(filepath.Join(filepath.Dir(filename), "extraDiagnosticMessages.json"))
|
|
|
|
maps.Copy(rawDiagnosticMessages, rawExtraMessages)
|
|
diagnosticMessages := slices.Collect(maps.Values(rawDiagnosticMessages))
|
|
|
|
slices.SortFunc(diagnosticMessages, func(a *diagnosticMessage, b *diagnosticMessage) int {
|
|
return cmp.Compare(a.Code, b.Code)
|
|
})
|
|
|
|
// Collect known keys for filtering localizations
|
|
knownKeys := make(map[string]bool, len(diagnosticMessages))
|
|
for _, m := range diagnosticMessages {
|
|
_, key := convertPropertyName(m.key, m.Code)
|
|
knownKeys[key] = true
|
|
}
|
|
|
|
// Generate diagnostics file
|
|
diagnosticsBuf := generateDiagnostics(diagnosticMessages)
|
|
|
|
formatted, err := format.Source(diagnosticsBuf.Bytes())
|
|
if err != nil {
|
|
log.Fatalf("failed to format diagnostics output: %v", err)
|
|
return
|
|
}
|
|
|
|
if err := os.WriteFile(*diagnosticsOutput, formatted, 0o666); err != nil {
|
|
log.Fatalf("failed to write diagnostics output: %v", err)
|
|
return
|
|
}
|
|
|
|
// Generate localizations file
|
|
locBuf := generateLocalizations(knownKeys, *locDir)
|
|
|
|
formatted, err = format.Source(locBuf.Bytes())
|
|
if err != nil {
|
|
log.Fatalf("failed to format localizations output: %v", err)
|
|
return
|
|
}
|
|
|
|
if err := os.WriteFile(*locOutput, formatted, 0o666); err != nil {
|
|
log.Fatalf("failed to write localizations output: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func generateDiagnostics(diagnosticMessages []*diagnosticMessage) *bytes.Buffer {
|
|
var buf bytes.Buffer
|
|
|
|
buf.WriteString("// Code generated by generate.go; DO NOT EDIT.\n")
|
|
buf.WriteString("\n")
|
|
buf.WriteString("package diagnostics\n")
|
|
|
|
for _, m := range diagnosticMessages {
|
|
varName, key := convertPropertyName(m.key, m.Code)
|
|
|
|
fmt.Fprintf(&buf, "var %s = &Message{code: %d, category: Category%s, key: %q, text: %q", varName, m.Code, m.Category, key, m.key)
|
|
|
|
if m.ReportsUnnecessary {
|
|
buf.WriteString(`, reportsUnnecessary: true`)
|
|
}
|
|
if m.ElidedInCompatibilityPyramid {
|
|
buf.WriteString(`, elidedInCompatibilityPyramid: true`)
|
|
}
|
|
if m.ReportsDeprecated {
|
|
buf.WriteString(`, reportsDeprecated: true`)
|
|
}
|
|
|
|
buf.WriteString("}\n\n")
|
|
}
|
|
|
|
buf.WriteString("func keyToMessage(key Key) *Message {\n")
|
|
buf.WriteString("\tswitch key {\n")
|
|
for _, m := range diagnosticMessages {
|
|
_, key := convertPropertyName(m.key, m.Code)
|
|
varName, _ := convertPropertyName(m.key, m.Code)
|
|
fmt.Fprintf(&buf, "\tcase %q:\n\t\treturn %s\n", key, varName)
|
|
}
|
|
buf.WriteString("\tdefault:\n\t\treturn nil\n")
|
|
buf.WriteString("\t}\n")
|
|
buf.WriteString("}\n")
|
|
|
|
return &buf
|
|
}
|
|
|
|
func generateLocalizations(knownKeys map[string]bool, locDir string) *bytes.Buffer {
|
|
var buf bytes.Buffer
|
|
|
|
buf.WriteString("// Code generated by generate.go; DO NOT EDIT.\n")
|
|
buf.WriteString("\n")
|
|
buf.WriteString("package diagnostics\n")
|
|
buf.WriteString("\n")
|
|
buf.WriteString("import (\n")
|
|
buf.WriteString("\t\"compress/gzip\"\n")
|
|
buf.WriteString("\t_ \"embed\"\n")
|
|
buf.WriteString("\t\"strings\"\n")
|
|
buf.WriteString("\t\"sync\"\n")
|
|
buf.WriteString("\t\"golang.org/x/text/language\"\n")
|
|
buf.WriteString("\t\"github.com/microsoft/typescript-go/internal/json\"\n")
|
|
buf.WriteString(")\n")
|
|
|
|
// Remove and recreate the loc directory for a clean state
|
|
if err := os.RemoveAll(locDir); err != nil {
|
|
log.Fatalf("failed to remove locale directory: %v", err)
|
|
}
|
|
if err := os.MkdirAll(locDir, 0o755); err != nil {
|
|
log.Fatalf("failed to create locale directory: %v", err)
|
|
}
|
|
|
|
// Generate locale maps
|
|
localeFiles, err := filepath.Glob(filepath.Join(repo.TypeScriptSubmodulePath(), "src", "loc", "lcl", "*", "diagnosticMessages", "diagnosticMessages.generated.json.lcl"))
|
|
if err != nil {
|
|
log.Fatalf("failed to find locale files: %v", err)
|
|
}
|
|
if len(localeFiles) == 0 {
|
|
log.Fatalf("no locale files found in %s", filepath.Join(repo.TypeScriptSubmodulePath(), "src", "loc", "lcl"))
|
|
}
|
|
slices.Sort(localeFiles)
|
|
|
|
type localeInfo struct {
|
|
varName string
|
|
tgtCul string
|
|
canonical string // canonical lowercase form (e.g., "zh-cn", "pt-br")
|
|
lang string
|
|
messages map[string]string
|
|
filename string
|
|
}
|
|
|
|
var locales []localeInfo
|
|
|
|
for _, localeFile := range localeFiles {
|
|
localizedMessages, tgtCul := readLocalizedMessages(localeFile)
|
|
if len(localizedMessages) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Filter to only known keys
|
|
for key := range localizedMessages {
|
|
if !knownKeys[key] {
|
|
delete(localizedMessages, key)
|
|
}
|
|
}
|
|
|
|
if len(localizedMessages) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Convert locale code to valid Go identifier
|
|
localeVar := strings.ReplaceAll(strings.ReplaceAll(tgtCul, "-", ""), "_", "")
|
|
|
|
// Parse the locale using language.Tag to get canonical forms
|
|
tag, err := language.Parse(tgtCul)
|
|
if err != nil {
|
|
log.Printf("failed to parse locale %q: %v", tgtCul, err)
|
|
continue
|
|
}
|
|
|
|
base, _ := tag.Base()
|
|
lang := strings.ToLower(base.String())
|
|
|
|
// Get canonical form (lowercase with dash)
|
|
canonical := strings.ToLower(tgtCul)
|
|
|
|
// Filename for the JSON.gz file (use the original tgtCul as standard language tag)
|
|
filename := fmt.Sprintf("%s.json.gz", tgtCul)
|
|
|
|
// Write the JSON.gz file
|
|
// Convert map to OrderedMap with sorted keys for consistent ordering
|
|
keys := slices.Sorted(maps.Keys(localizedMessages))
|
|
var orderedMessages collections.OrderedMap[string, string]
|
|
for _, key := range keys {
|
|
orderedMessages.Set(key, localizedMessages[key])
|
|
}
|
|
jsonData, err := json.Marshal(&orderedMessages)
|
|
if err != nil {
|
|
log.Fatalf("failed to marshal locale %s: %v", tgtCul, err)
|
|
}
|
|
|
|
var compressed bytes.Buffer
|
|
gzipWriter, err := gzip.NewWriterLevel(&compressed, gzip.BestCompression)
|
|
if err != nil {
|
|
log.Fatalf("failed to create gzip writer for locale %s: %v", tgtCul, err)
|
|
}
|
|
if _, err := gzipWriter.Write(jsonData); err != nil {
|
|
log.Fatalf("failed to compress locale %s: %v", tgtCul, err)
|
|
}
|
|
if err := gzipWriter.Close(); err != nil {
|
|
log.Fatalf("failed to close gzip writer for locale %s: %v", tgtCul, err)
|
|
}
|
|
|
|
outputPath := filepath.Join(locDir, filename)
|
|
if err := os.WriteFile(outputPath, compressed.Bytes(), 0o644); err != nil {
|
|
log.Fatalf("failed to write locale file %s: %v", outputPath, err)
|
|
}
|
|
|
|
locales = append(locales, localeInfo{
|
|
varName: localeVar,
|
|
tgtCul: tgtCul,
|
|
canonical: canonical,
|
|
lang: lang,
|
|
messages: localizedMessages,
|
|
filename: filename,
|
|
})
|
|
}
|
|
|
|
// Generate matcher with inlined tags
|
|
// English is first (index 0) as the default/fallback with no translation needed
|
|
buf.WriteString("\nvar matcher = language.NewMatcher([]language.Tag{\n")
|
|
buf.WriteString("\tlanguage.English,\n")
|
|
for _, loc := range locales {
|
|
fmt.Fprintf(&buf, "\tlanguage.MustParse(%q),\n", loc.tgtCul)
|
|
}
|
|
buf.WriteString("})\n")
|
|
|
|
// Generate index-to-function map for matcher results
|
|
// English (index 0) returns nil since we use the default English text
|
|
buf.WriteString("\nvar localeFuncs = []func() map[Key]string{\n")
|
|
buf.WriteString("\tnil, // English (default)\n")
|
|
for _, loc := range locales {
|
|
fmt.Fprintf(&buf, "\t%s,\n", loc.varName)
|
|
}
|
|
buf.WriteString("}\n")
|
|
|
|
// Generate helper function for decompressing locale data
|
|
buf.WriteString("\nfunc loadLocaleData(data string) map[Key]string {\n")
|
|
buf.WriteString("\tgr, err := gzip.NewReader(strings.NewReader(data))\n")
|
|
buf.WriteString("\tif err != nil {\n")
|
|
buf.WriteString("\t\tpanic(\"failed to create gzip reader: \" + err.Error())\n")
|
|
buf.WriteString("\t}\n")
|
|
buf.WriteString("\tdefer gr.Close()\n")
|
|
buf.WriteString("\tvar result map[Key]string\n")
|
|
buf.WriteString("\tif err := json.UnmarshalRead(gr, &result); err != nil {\n")
|
|
buf.WriteString("\t\tpanic(\"failed to unmarshal locale data: \" + err.Error())\n")
|
|
buf.WriteString("\t}\n")
|
|
buf.WriteString("\treturn result\n")
|
|
buf.WriteString("}\n")
|
|
|
|
// Generate embed directives, vars, and loader functions interleaved at the bottom
|
|
for _, loc := range locales {
|
|
fmt.Fprintf(&buf, "\n//go:embed loc/%s\n", loc.filename)
|
|
fmt.Fprintf(&buf, "var %sData string\n", loc.varName)
|
|
fmt.Fprintf(&buf, "\nvar %s = sync.OnceValue(func() map[Key]string {\n", loc.varName)
|
|
fmt.Fprintf(&buf, "\treturn loadLocaleData(%sData)\n", loc.varName)
|
|
buf.WriteString("})\n")
|
|
}
|
|
|
|
return &buf
|
|
}
|
|
|
|
func readRawMessages(p string) map[int]*diagnosticMessage {
|
|
file, err := os.Open(p)
|
|
if err != nil {
|
|
log.Fatalf("failed to open file: %v", err)
|
|
return nil
|
|
}
|
|
defer file.Close()
|
|
|
|
var rawMessages map[string]*diagnosticMessage
|
|
if err := json.UnmarshalRead(file, &rawMessages); err != nil {
|
|
log.Fatalf("failed to decode file: %v", err)
|
|
return nil
|
|
}
|
|
|
|
codeToMessage := make(map[int]*diagnosticMessage, len(rawMessages))
|
|
for k, m := range rawMessages {
|
|
m.key = k
|
|
codeToMessage[m.Code] = m
|
|
}
|
|
|
|
return codeToMessage
|
|
}
|
|
|
|
func readLocalizedMessages(p string) (map[string]string, string) {
|
|
file, err := os.Open(p)
|
|
if err != nil {
|
|
log.Printf("failed to open locale file %s: %v", p, err)
|
|
return nil, ""
|
|
}
|
|
defer file.Close()
|
|
|
|
var lcx LCX
|
|
if err := xml.NewDecoder(file).Decode(&lcx); err != nil {
|
|
log.Printf("failed to decode locale file %s: %v", p, err)
|
|
return nil, ""
|
|
}
|
|
|
|
messages := make(map[string]string)
|
|
|
|
// Navigate the nested Item structure
|
|
for _, rootItem := range lcx.RootItems {
|
|
for _, stringTable := range rootItem.Items {
|
|
for _, item := range stringTable.Items {
|
|
// ItemId has format ";key_code", remove the leading semicolon
|
|
itemId := strings.TrimPrefix(item.ItemId, ";")
|
|
|
|
// Get the localized text from Tgt if available, otherwise use Val
|
|
var text string
|
|
if item.Str.Tgt != nil && item.Str.Tgt.Val != "" {
|
|
text = item.Str.Tgt.Val
|
|
} else {
|
|
text = item.Str.Val
|
|
}
|
|
|
|
messages[itemId] = text
|
|
}
|
|
}
|
|
}
|
|
|
|
return messages, lcx.TgtCul
|
|
}
|
|
|
|
var (
|
|
multipleUnderscoreRegexp = regexp.MustCompile(`_+`)
|
|
leadingUnderscoreUnlessDigitRegexp = regexp.MustCompile(`^_+(\D)`)
|
|
trailingUnderscoreRegexp = regexp.MustCompile(`_$`)
|
|
)
|
|
|
|
func convertPropertyName(origName string, code int) (varName string, key string) {
|
|
var b strings.Builder
|
|
b.Grow(len(origName))
|
|
|
|
for _, r := range origName {
|
|
switch r {
|
|
case '*':
|
|
b.WriteString("_Asterisk")
|
|
case '/':
|
|
b.WriteString("_Slash")
|
|
case ':':
|
|
b.WriteString("_Colon")
|
|
default:
|
|
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
|
|
b.WriteRune('_')
|
|
} else {
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
}
|
|
|
|
varName = b.String()
|
|
// get rid of all multi-underscores
|
|
varName = multipleUnderscoreRegexp.ReplaceAllString(varName, "_")
|
|
// remove any leading underscore, unless it is followed by a number.
|
|
varName = leadingUnderscoreUnlessDigitRegexp.ReplaceAllString(varName, "$1")
|
|
// get rid of all trailing underscores.
|
|
varName = trailingUnderscoreRegexp.ReplaceAllString(varName, "")
|
|
|
|
key = varName
|
|
if len(key) > 100 {
|
|
key = key[:100]
|
|
}
|
|
key = key + "_" + strconv.Itoa(code)
|
|
|
|
if !token.IsExported(varName) {
|
|
var b strings.Builder
|
|
b.Grow(len(varName) + 2)
|
|
if varName[0] == '_' {
|
|
b.WriteString("X")
|
|
} else {
|
|
b.WriteString("X_")
|
|
}
|
|
b.WriteString(varName)
|
|
varName = b.String()
|
|
}
|
|
|
|
if !token.IsIdentifier(varName) || !token.IsExported(varName) {
|
|
log.Fatalf("failed to convert property name to exported identifier: %q", origName)
|
|
}
|
|
|
|
return varName, key
|
|
}
|