2026-01-01 21:05:54 +00:00

280 lines
6.4 KiB
Go

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
"bufio"
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
type POFile struct {
HeaderLines []string
Entries []POEntry
}
type POEntry struct {
Comments []string
References []string
MsgID string
MsgStr string
}
func main() {
localesDir := flag.String("locales-dir", "../../../../src/locales", "Path to the locales directory")
singleLocale := flag.String("locale", "", "Reset only this locale (empty = all)")
dryRun := flag.Bool("dry-run", false, "Show what would be reset without making changes")
flag.Parse()
absLocalesDir, err := absPath(*localesDir)
if err != nil {
fmt.Printf("Failed to resolve locales directory: %v\n", err)
os.Exit(1)
}
locales, err := discoverLocales(absLocalesDir)
if err != nil {
fmt.Printf("Failed to discover locales: %v\n", err)
os.Exit(1)
}
var targetLocales []string
for _, locale := range locales {
if locale == "en-US" {
continue
}
if *singleLocale != "" && locale != *singleLocale {
continue
}
targetLocales = append(targetLocales, locale)
}
if len(targetLocales) == 0 {
fmt.Println("No target locales found")
os.Exit(1)
}
fmt.Printf("Resetting translations for %d locales...\n", len(targetLocales))
if *dryRun {
fmt.Println("(DRY RUN - no changes will be made)")
}
fmt.Println()
totalReset := 0
for _, locale := range targetLocales {
poPath := filepath.Join(absLocalesDir, locale, "messages.po")
poFile, err := parsePOFile(poPath)
if err != nil {
fmt.Printf(" ✗ %s: failed to parse: %v\n", locale, err)
continue
}
resetCount := 0
for i := range poFile.Entries {
if poFile.Entries[i].MsgStr != "" {
resetCount++
poFile.Entries[i].MsgStr = ""
}
}
if resetCount == 0 {
fmt.Printf(" - %s: already empty (0 strings)\n", locale)
continue
}
if !*dryRun {
if err := writePOFile(poPath, poFile); err != nil {
fmt.Printf(" ✗ %s: failed to write: %v\n", locale, err)
continue
}
}
fmt.Printf(" ✓ %s: reset %d strings\n", locale, resetCount)
totalReset += resetCount
}
fmt.Printf("\nTotal: reset %d translations across %d locales\n", totalReset, len(targetLocales))
if *dryRun {
fmt.Println("(DRY RUN - run without --dry-run to apply changes)")
}
}
func absPath(rel string) (string, error) {
if filepath.IsAbs(rel) {
return rel, nil
}
wd, err := os.Getwd()
if err != nil {
return "", err
}
return filepath.Join(wd, rel), nil
}
func discoverLocales(localesDir string) ([]string, error) {
entries, err := os.ReadDir(localesDir)
if err != nil {
return nil, err
}
var locales []string
for _, entry := range entries {
if entry.IsDir() {
locales = append(locales, entry.Name())
}
}
sort.Strings(locales)
return locales, nil
}
func parsePOFile(path string) (POFile, error) {
file, err := os.Open(path)
if err != nil {
return POFile{}, err
}
defer file.Close()
var (
current []string
scanner = bufio.NewScanner(file)
trimmed string
headerSet bool
result POFile
)
for scanner.Scan() {
line := scanner.Text()
trimmed = strings.TrimSpace(line)
if trimmed == "" {
if len(current) > 0 {
entry := parseBlock(current)
if !headerSet && entry.MsgID == "" {
result.HeaderLines = append([]string{}, current...)
headerSet = true
} else {
result.Entries = append(result.Entries, entry)
}
current = nil
}
continue
}
current = append(current, line)
}
if len(current) > 0 {
entry := parseBlock(current)
if !headerSet && entry.MsgID == "" {
result.HeaderLines = append([]string{}, current...)
} else {
result.Entries = append(result.Entries, entry)
}
}
if err := scanner.Err(); err != nil {
return POFile{}, err
}
return result, nil
}
func parseBlock(lines []string) POEntry {
entry := POEntry{}
var (
inMsgID bool
inMsgStr bool
)
for _, raw := range lines {
line := strings.TrimSpace(raw)
switch {
case strings.HasPrefix(line, "#."):
entry.Comments = append(entry.Comments, strings.TrimSpace(line[2:]))
case strings.HasPrefix(line, "#:"):
entry.References = append(entry.References, strings.TrimSpace(line[2:]))
case strings.HasPrefix(line, "msgid"):
entry.MsgID = parseQuoted(strings.TrimSpace(line[len("msgid"):]))
inMsgID = true
inMsgStr = false
case strings.HasPrefix(line, "msgstr"):
entry.MsgStr = parseQuoted(strings.TrimSpace(line[len("msgstr"):]))
inMsgStr = true
inMsgID = false
case strings.HasPrefix(line, "\""):
if inMsgID {
entry.MsgID += parseQuoted(line)
} else if inMsgStr {
entry.MsgStr += parseQuoted(line)
}
}
}
return entry
}
func parseQuoted(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
if unquoted, err := strconv.Unquote(value); err == nil {
return unquoted
}
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
return value[1 : len(value)-1]
}
return value
}
func writePOFile(path string, po POFile) error {
var lines []string
if len(po.HeaderLines) > 0 {
lines = append(lines, po.HeaderLines...)
lines = append(lines, "")
}
for idx, entry := range po.Entries {
lines = append(lines, renderEntry(entry))
if idx < len(po.Entries)-1 {
lines = append(lines, "")
}
}
lines = append(lines, "")
return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0o644)
}
func renderEntry(entry POEntry) string {
var sb strings.Builder
for _, comment := range entry.Comments {
sb.WriteString("#. ")
sb.WriteString(comment)
sb.WriteString("\n")
}
for _, ref := range entry.References {
sb.WriteString("#: ")
sb.WriteString(ref)
sb.WriteString("\n")
}
sb.WriteString("msgid ")
sb.WriteString(strconv.Quote(entry.MsgID))
sb.WriteString("\nmsgstr ")
sb.WriteString(strconv.Quote(entry.MsgStr))
return sb.String()
}