Skip to content

Instantly share code, notes, and snippets.

@hawx
Created January 12, 2023 08:28
Show Gist options
  • Save hawx/b52354c509d838e4ae233c07fc2a3ec6 to your computer and use it in GitHub Desktop.
Save hawx/b52354c509d838e4ae233c07fc2a3ec6 to your computer and use it in GitHub Desktop.
opg-modernising-lpa translation tool

This tool will generate a CSV report from language files of each key, its variations, and when each was modified (via git blame).

translation-tool -git-repo ~/projects/opg-modernising-lpa ~/projects/opg-modernising-lpa/app/lang/* > ~/Desktop/translation-report.csv
package main
import (
"bufio"
"bytes"
"encoding/csv"
"encoding/json"
"flag"
"os"
"os/exec"
"path"
"strconv"
"strings"
"time"
)
func main() {
gitRepo := flag.String("git-repo", "", "Path to git repository")
flag.Parse()
files := flag.Args()
translationStrings := map[string]map[string]translationString{}
variationKeys := map[string]map[string]struct{}{}
blames := map[string]map[string]blame{}
langs := []string{}
for _, file := range files {
f, _ := os.Open(file)
defer f.Close()
lang, _, _ := strings.Cut(path.Base(file), ".")
langs = append(langs, lang)
variationKeys[lang] = map[string]struct{}{}
var v map[string]translationString
json.NewDecoder(f).Decode(&v)
for key, s := range v {
if _, ok := translationStrings[key]; !ok {
translationStrings[key] = map[string]translationString{}
}
translationStrings[key][lang] = s
for k := range s.Variations {
variationKeys[lang][k] = struct{}{}
}
}
for key, b := range runBlame(*gitRepo, file) {
if _, ok := blames[key]; !ok {
blames[key] = map[string]blame{}
}
blames[key][lang] = b
}
}
w := csv.NewWriter(os.Stdout)
defer w.Flush()
cols := []string{"key"}
for _, lang := range langs {
cols = append(cols, lang+"-changed", lang)
for variation := range variationKeys[lang] {
cols = append(cols, lang+"-"+variation)
}
}
w.Write(cols)
for key, ss := range translationStrings {
row := []string{key}
for _, lang := range langs {
row = append(row, blames[key][lang].At.Format(time.RFC3339))
row = append(row, ss[lang].Value)
for variation := range variationKeys[lang] {
row = append(row, ss[lang].Variations[variation])
}
}
w.Write(row)
}
}
type translationString struct {
Value string
Variations map[string]string
}
func (s *translationString) UnmarshalJSON(b []byte) error {
var value string
if err := json.Unmarshal(b, &value); err == nil {
s.Value = value
return nil
}
var variations map[string]string
if err := json.Unmarshal(b, &variations); err == nil {
s.Variations = variations
return nil
}
return nil
}
type blame struct {
Hash string
Author string
At time.Time
}
type blameRow struct {
Hash string
Author string
At time.Time
Keys []string
}
const (
authorPrefix = "author "
authorTimePrefix = "author-time "
keyPrefix = "\t "
)
func runBlame(gitRepo, file string) map[string]blame {
cmd := exec.Command("git", "-C", gitRepo, "blame", "--line-porcelain", "--", file)
data, _ := cmd.Output()
r := bufio.NewScanner(bytes.NewReader(data))
rows := []blameRow{}
currentRow := blameRow{}
var nestedKey string
blameInfos := map[string]blame{}
for r.Scan() {
line := r.Text()
if strings.HasPrefix(line, authorPrefix) {
rows = append(rows, currentRow) // start a new row
currentRow = blameRow{}
currentRow.Author = line[len(authorPrefix):]
} else if strings.HasPrefix(line, authorTimePrefix) {
t, _ := strconv.ParseInt(line[len(authorTimePrefix):], 10, 64)
currentRow.At = time.Unix(t, 0)
} else if strings.HasPrefix(line, keyPrefix) {
if strings.TrimSpace(line) == "}," {
nestedKey = ""
continue
}
quotedKey, _, _ := strings.Cut(line[len(keyPrefix):], ":")
key := quotedKey[1 : len(quotedKey)-1]
if strings.HasPrefix(line[len(keyPrefix):], " ") {
key = nestedKey
}
currentRow.Keys = append(currentRow.Keys, key)
blameInfos[key] = blame{
At: currentRow.At,
Hash: currentRow.Hash,
Author: currentRow.Author,
}
if strings.HasSuffix(strings.TrimSpace(line), "{") {
nestedKey = key
}
}
}
return blameInfos
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment