Initial open source release
This commit is contained in:
commit
74992e893f
377 changed files with 118084 additions and 0 deletions
109
internal/app/langpack/parser.go
Normal file
109
internal/app/langpack/parser.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package langpack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
var tdesktopStringRE = regexp.MustCompile(`(?s)"((?:\\.|[^"\\])*)"\s*=\s*"((?:\\.|[^"\\])*)";`)
|
||||
|
||||
// ParseTDesktopFile 解析 TDesktop .strings 文件为 domain 语言包。
|
||||
func ParseTDesktopFile(path string) (domain.LangPack, error) {
|
||||
pack, err := packFromFilename(path)
|
||||
if err != nil {
|
||||
return domain.LangPack{}, err
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return domain.LangPack{}, fmt.Errorf("read tdesktop langpack %q: %w", path, err)
|
||||
}
|
||||
|
||||
plain := make([]domain.LangPackString, 0)
|
||||
plurals := make(map[string]*domain.LangPackString)
|
||||
pluralOrder := make([]string, 0)
|
||||
for _, match := range tdesktopStringRE.FindAllStringSubmatch(string(data), -1) {
|
||||
key := unquoteTDesktop(match[1])
|
||||
value := unquoteTDesktop(match[2])
|
||||
base, plural := splitPluralKey(key)
|
||||
if plural == "" {
|
||||
plain = append(plain, domain.LangPackString{Key: key, Value: value})
|
||||
continue
|
||||
}
|
||||
item, ok := plurals[base]
|
||||
if !ok {
|
||||
plurals[base] = &domain.LangPackString{Key: base, Pluralized: true}
|
||||
item = plurals[base]
|
||||
pluralOrder = append(pluralOrder, base)
|
||||
}
|
||||
setPluralValue(item, plural, value)
|
||||
}
|
||||
|
||||
pack.Strings = make([]domain.LangPackString, 0, len(plain)+len(pluralOrder))
|
||||
pack.Strings = append(pack.Strings, plain...)
|
||||
for _, key := range pluralOrder {
|
||||
pack.Strings = append(pack.Strings, *plurals[key])
|
||||
}
|
||||
return pack, nil
|
||||
}
|
||||
|
||||
func packFromFilename(path string) (domain.LangPack, error) {
|
||||
name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||
const prefix = "tdesktop_"
|
||||
if !strings.HasPrefix(name, prefix) {
|
||||
return domain.LangPack{}, fmt.Errorf("invalid tdesktop langpack filename %q", filepath.Base(path))
|
||||
}
|
||||
rest := strings.TrimPrefix(name, prefix)
|
||||
idx := strings.LastIndex(rest, "_v")
|
||||
if idx <= 0 || idx+2 >= len(rest) {
|
||||
return domain.LangPack{}, fmt.Errorf("invalid tdesktop langpack filename %q", filepath.Base(path))
|
||||
}
|
||||
version, err := strconv.Atoi(rest[idx+2:])
|
||||
if err != nil {
|
||||
return domain.LangPack{}, fmt.Errorf("parse langpack version %q: %w", rest[idx+2:], err)
|
||||
}
|
||||
return domain.LangPack{
|
||||
LangPack: "tdesktop",
|
||||
LangCode: rest[:idx],
|
||||
Version: version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func splitPluralKey(key string) (base, plural string) {
|
||||
for _, suffix := range []string{"#zero", "#one", "#two", "#few", "#many", "#other"} {
|
||||
if strings.HasSuffix(key, suffix) {
|
||||
return strings.TrimSuffix(key, suffix), strings.TrimPrefix(suffix, "#")
|
||||
}
|
||||
}
|
||||
return key, ""
|
||||
}
|
||||
|
||||
func setPluralValue(item *domain.LangPackString, plural, value string) {
|
||||
switch plural {
|
||||
case "zero":
|
||||
item.ZeroValue = value
|
||||
case "one":
|
||||
item.OneValue = value
|
||||
case "two":
|
||||
item.TwoValue = value
|
||||
case "few":
|
||||
item.FewValue = value
|
||||
case "many":
|
||||
item.ManyValue = value
|
||||
case "other":
|
||||
item.OtherValue = value
|
||||
}
|
||||
}
|
||||
|
||||
func unquoteTDesktop(s string) string {
|
||||
v, err := strconv.Unquote(`"` + s + `"`)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return v
|
||||
}
|
||||
37
internal/app/langpack/parser_test.go
Normal file
37
internal/app/langpack/parser_test.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package langpack
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseTDesktopFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "tdesktop_en_v42.strings")
|
||||
if err := os.WriteFile(path, []byte(`
|
||||
"lng_plain" = "Plain value";
|
||||
"lng_escape" = "Line\nTwo";
|
||||
"lng_items#one" = "{count} item";
|
||||
"lng_items#other" = "{count} items";
|
||||
`), 0o600); err != nil {
|
||||
t.Fatalf("write fixture: %v", err)
|
||||
}
|
||||
|
||||
pack, err := ParseTDesktopFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if pack.LangPack != "tdesktop" || pack.LangCode != "en" || pack.Version != 42 {
|
||||
t.Fatalf("pack meta = %+v", pack)
|
||||
}
|
||||
if len(pack.Strings) != 3 {
|
||||
t.Fatalf("strings count = %d, want 3", len(pack.Strings))
|
||||
}
|
||||
if got := pack.Strings[1].Value; got != "Line\nTwo" {
|
||||
t.Fatalf("escape value = %q", got)
|
||||
}
|
||||
plural := pack.Strings[2]
|
||||
if !plural.Pluralized || plural.Key != "lng_items" || plural.OneValue == "" || plural.OtherValue == "" {
|
||||
t.Fatalf("plural string = %+v", plural)
|
||||
}
|
||||
}
|
||||
56
internal/app/langpack/seed.go
Normal file
56
internal/app/langpack/seed.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package langpack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SeedDirectory 将导出的 .strings 文件导入 LangPackStore。
|
||||
// root 可直接指向 data/langpack,也可指向包含 .strings 的具体平台目录。
|
||||
func (s *Service) SeedDirectory(ctx context.Context, root string) (int, error) {
|
||||
if s == nil || s.packs == nil || root == "" {
|
||||
return 0, nil
|
||||
}
|
||||
dir := filepath.Clean(root)
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("stat langpack seed dir: %w", err)
|
||||
}
|
||||
tdesktopDir := filepath.Join(dir, "tdesktop")
|
||||
if info, err := os.Stat(tdesktopDir); err == nil && info.IsDir() {
|
||||
dir = tdesktopDir
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read langpack seed dir: %w", err)
|
||||
}
|
||||
seeded := 0
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".strings") {
|
||||
continue
|
||||
}
|
||||
pack, err := ParseTDesktopFile(filepath.Join(dir, entry.Name()))
|
||||
if err != nil {
|
||||
return seeded, err
|
||||
}
|
||||
existing, err := s.packs.GetPack(ctx, pack.LangPack, pack.LangCode, pack.Version)
|
||||
if err != nil {
|
||||
return seeded, err
|
||||
}
|
||||
if existing.Version >= pack.Version {
|
||||
continue
|
||||
}
|
||||
if err := s.packs.UpsertPack(ctx, pack); err != nil {
|
||||
return seeded, err
|
||||
}
|
||||
seeded += len(pack.Strings)
|
||||
}
|
||||
return seeded, nil
|
||||
}
|
||||
53
internal/app/langpack/service.go
Normal file
53
internal/app/langpack/service.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package langpack
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
// Service 提供客户端语言包查询。
|
||||
type Service struct {
|
||||
packs store.LangPackStore
|
||||
}
|
||||
|
||||
// NewService 创建 langpack 服务。
|
||||
func NewService(packs store.LangPackStore) *Service {
|
||||
return &Service{packs: packs}
|
||||
}
|
||||
|
||||
// GetLangPack 返回完整语言包。
|
||||
func (s *Service) GetLangPack(ctx context.Context, langPack, langCode string) (domain.LangPack, error) {
|
||||
return s.GetDifference(ctx, langPack, langCode, 0)
|
||||
}
|
||||
|
||||
// GetDifference 返回从 fromVersion 到当前版本的语言包差异。
|
||||
func (s *Service) GetDifference(ctx context.Context, langPack, langCode string, fromVersion int) (domain.LangPack, error) {
|
||||
if s == nil || s.packs == nil {
|
||||
return domain.LangPack{LangPack: langPack, LangCode: langCode, FromVersion: fromVersion}, nil
|
||||
}
|
||||
return s.packs.GetPack(ctx, normalizePack(langPack), normalizeCode(langCode), fromVersion)
|
||||
}
|
||||
|
||||
// GetStrings 返回指定 key 的语言包字符串。
|
||||
func (s *Service) GetStrings(ctx context.Context, langPack, langCode string, keys []string) (domain.LangPack, error) {
|
||||
if s == nil || s.packs == nil {
|
||||
return domain.LangPack{LangPack: langPack, LangCode: langCode}, nil
|
||||
}
|
||||
return s.packs.GetStrings(ctx, normalizePack(langPack), normalizeCode(langCode), keys)
|
||||
}
|
||||
|
||||
func normalizePack(langPack string) string {
|
||||
if langPack == "" {
|
||||
return "tdesktop"
|
||||
}
|
||||
return langPack
|
||||
}
|
||||
|
||||
func normalizeCode(langCode string) string {
|
||||
if langCode == "" {
|
||||
return "en"
|
||||
}
|
||||
return langCode
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue