feat: sync langpack and auth updates

Sync telesrv commits 49f9bab and 04d9563 into the public mirror. Exclude private docs and runtime key material per sync rules.
This commit is contained in:
A 2026-07-18 00:07:41 +08:00
parent 6af61f26ba
commit 1292540350
244 changed files with 1552172 additions and 27668 deletions

View file

@ -2,6 +2,7 @@ package postgres
import (
"context"
"encoding/json"
"errors"
"fmt"
@ -68,6 +69,68 @@ func (s *LangPackStore) GetStrings(ctx context.Context, langPack, langCode strin
return meta, nil
}
func (s *LangPackStore) ListLanguages(ctx context.Context, langPack string) ([]domain.LangPackLanguage, error) {
rows, err := s.q.ListLangPackLanguages(ctx, langPack)
if err != nil {
return nil, fmt.Errorf("list lang pack languages: %w", err)
}
out := make([]domain.LangPackLanguage, 0, len(rows))
for _, row := range rows {
out = append(out, domain.LangPackLanguage{
LangPack: row.LangPack,
LangCode: row.LangCode,
Name: row.Name,
NativeName: row.NativeName,
StringsCount: int(row.StringsCount),
TranslatedCount: int(row.StringsCount),
})
}
return out, nil
}
func (s *LangPackStore) GetSeedCatalog(ctx context.Context, catalog string) (domain.LangPackSeedCatalog, error) {
if catalog == "" {
catalog = "default"
}
encoded, err := s.q.GetLangPackSeedHash(ctx, langPackSeedManifestStateKey(catalog))
if errors.Is(err, pgx.ErrNoRows) {
return domain.LangPackSeedCatalog{Catalog: catalog}, nil
}
if err != nil {
return domain.LangPackSeedCatalog{}, fmt.Errorf("get langpack seed catalog %q: %w", catalog, err)
}
var state domain.LangPackSeedCatalog
if err := json.Unmarshal([]byte(encoded), &state); err != nil {
return domain.LangPackSeedCatalog{}, fmt.Errorf("decode langpack seed catalog %q: %w", catalog, err)
}
if state.Catalog != catalog {
return domain.LangPackSeedCatalog{}, fmt.Errorf("langpack seed catalog key %q contains catalog %q", catalog, state.Catalog)
}
return state, nil
}
func (s *LangPackStore) ReconcileSeed(ctx context.Context, seed domain.LangPackSeed) (int, error) {
txer, ok := s.db.(interface {
Begin(context.Context) (pgx.Tx, error)
})
if !ok {
return 0, errors.New("langpack seed reconciliation requires transaction support")
}
tx, err := txer.Begin(ctx)
if err != nil {
return 0, fmt.Errorf("begin langpack seed reconciliation: %w", err)
}
written, err := reconcileSeedWith(ctx, tx, s.q.WithTx(tx), seed)
if err != nil {
_ = tx.Rollback(ctx)
return 0, err
}
if err := tx.Commit(ctx); err != nil {
return 0, fmt.Errorf("commit langpack seed reconciliation: %w", err)
}
return written, nil
}
func (s *LangPackStore) UpsertPack(ctx context.Context, pack domain.LangPack) error {
if txer, ok := s.db.(interface {
Begin(context.Context) (pgx.Tx, error)
@ -77,7 +140,15 @@ func (s *LangPackStore) UpsertPack(ctx context.Context, pack domain.LangPack) er
return fmt.Errorf("begin lang pack upsert: %w", err)
}
q := s.q.WithTx(tx)
if err := upsertPackWith(ctx, q, pack); err != nil {
if err := replacePackWithCopy(ctx, tx, q, pack); err != nil {
_ = tx.Rollback(ctx)
return err
}
if err := q.DeleteLangPackSeedHash(ctx, langPackSeedStateKey(pack.LangPack, pack.LangCode)); err != nil {
_ = tx.Rollback(ctx)
return fmt.Errorf("delete lang pack seed hash: %w", err)
}
if err := invalidateLangPackSeedCatalogs(ctx, tx); err != nil {
_ = tx.Rollback(ctx)
return err
}
@ -86,10 +157,189 @@ func (s *LangPackStore) UpsertPack(ctx context.Context, pack domain.LangPack) er
}
return nil
}
return upsertPackWith(ctx, s.q, pack)
if err := replacePackWithQueries(ctx, s.q, pack); err != nil {
return err
}
if err := s.q.DeleteLangPackSeedHash(ctx, langPackSeedStateKey(pack.LangPack, pack.LangCode)); err != nil {
return fmt.Errorf("delete lang pack seed hash: %w", err)
}
if err := invalidateLangPackSeedCatalogs(ctx, s.db); err != nil {
return err
}
return nil
}
func upsertPackWith(ctx context.Context, q *sqlcgen.Queries, pack domain.LangPack) error {
func reconcileSeedWith(ctx context.Context, tx pgx.Tx, q *sqlcgen.Queries, seed domain.LangPackSeed) (int, error) {
catalog := seed.Catalog
if catalog == "" {
catalog = "default"
}
reconciledScopes := append([]string(nil), seed.Scopes...)
scopes := make(map[string]struct{}, len(seed.Scopes))
wanted := make(map[string]map[string]struct{}, len(seed.Scopes))
for _, scope := range seed.Scopes {
scopes[scope] = struct{}{}
wanted[scope] = make(map[string]struct{})
}
previousScopesJSON, err := q.GetLangPackSeedHash(ctx, langPackSeedCatalogStateKey(catalog))
if err == nil {
var previousScopes []string
if err := json.Unmarshal([]byte(previousScopesJSON), &previousScopes); err != nil {
return 0, fmt.Errorf("decode previous langpack seed scopes for %q: %w", catalog, err)
}
for _, scope := range previousScopes {
if _, exists := scopes[scope]; exists {
continue
}
scopes[scope] = struct{}{}
wanted[scope] = make(map[string]struct{})
reconciledScopes = append(reconciledScopes, scope)
}
} else if !errors.Is(err, pgx.ErrNoRows) {
return 0, fmt.Errorf("get previous langpack seed scopes for %q: %w", catalog, err)
}
for _, entry := range seed.Packs {
if err := validateSeedEntry(entry); err != nil {
return 0, err
}
if _, ok := scopes[entry.Pack.LangPack]; !ok {
return 0, fmt.Errorf("seeded langpack %s/%s is outside reconciliation scopes", entry.Pack.LangPack, entry.Pack.LangCode)
}
if _, exists := wanted[entry.Pack.LangPack][entry.Pack.LangCode]; exists {
return 0, fmt.Errorf("duplicate seeded langpack %s/%s", entry.Pack.LangPack, entry.Pack.LangCode)
}
wanted[entry.Pack.LangPack][entry.Pack.LangCode] = struct{}{}
}
for _, scope := range reconciledScopes {
codes, err := q.ListLangPackCodes(ctx, scope)
if err != nil {
return 0, fmt.Errorf("list existing langpack codes for %q: %w", scope, err)
}
for _, code := range codes {
if _, keep := wanted[scope][code]; keep {
continue
}
params := sqlcgen.DeleteLangPackStringsParams{LangPack: scope, LangCode: code}
if err := q.DeleteLangPackStrings(ctx, params); err != nil {
return 0, fmt.Errorf("delete removed langpack strings %s/%s: %w", scope, code, err)
}
if err := q.DeleteLangPackMeta(ctx, sqlcgen.DeleteLangPackMetaParams(params)); err != nil {
return 0, fmt.Errorf("delete removed langpack metadata %s/%s: %w", scope, code, err)
}
if err := q.DeleteLangPackSeedHash(ctx, langPackSeedStateKey(scope, code)); err != nil {
return 0, fmt.Errorf("delete removed langpack seed hash %s/%s: %w", scope, code, err)
}
}
}
written := 0
for _, entry := range seed.Packs {
pack := entry.Pack
meta, metaErr := q.GetLangPackMeta(ctx, sqlcgen.GetLangPackMetaParams{
LangPack: pack.LangPack,
LangCode: pack.LangCode,
})
metaFound := metaErr == nil
if metaErr != nil && !errors.Is(metaErr, pgx.ErrNoRows) {
return 0, fmt.Errorf("get existing langpack metadata %s/%s: %w", pack.LangPack, pack.LangCode, metaErr)
}
if metaFound && pack.Version < int(meta.Version) {
return 0, fmt.Errorf("langpack version rollback %s/%s: %d < %d", pack.LangPack, pack.LangCode, pack.Version, meta.Version)
}
stateKey := langPackSeedStateKey(pack.LangPack, pack.LangCode)
oldHash, hashErr := q.GetLangPackSeedHash(ctx, stateKey)
hashFound := hashErr == nil
if hashErr != nil && !errors.Is(hashErr, pgx.ErrNoRows) {
return 0, fmt.Errorf("get langpack seed hash %s/%s: %w", pack.LangPack, pack.LangCode, hashErr)
}
if metaFound && hashFound && pack.Version == int(meta.Version) && oldHash != entry.ContentHash {
return 0, fmt.Errorf("langpack %s/%s v%d content changed without version bump", pack.LangPack, pack.LangCode, pack.Version)
}
if metaFound && hashFound && pack.Version == int(meta.Version) && int(meta.StringsCount) == entry.StringsCount && oldHash == entry.ContentHash {
continue
}
if !entry.ContentLoaded {
return 0, fmt.Errorf("langpack %s/%s content is required but was not loaded", pack.LangPack, pack.LangCode)
}
if err := replacePackWithCopy(ctx, tx, q, pack); err != nil {
return 0, err
}
if err := q.PutLangPackSeedHash(ctx, sqlcgen.PutLangPackSeedHashParams{Key: stateKey, ContentHash: entry.ContentHash}); err != nil {
return 0, fmt.Errorf("put langpack seed hash %s/%s: %w", pack.LangPack, pack.LangCode, err)
}
written += len(pack.Strings)
}
encodedScopes, err := json.Marshal(seed.Scopes)
if err != nil {
return 0, fmt.Errorf("encode langpack seed scopes for %q: %w", catalog, err)
}
if err := q.PutLangPackSeedHash(ctx, sqlcgen.PutLangPackSeedHashParams{
Key: langPackSeedCatalogStateKey(catalog),
ContentHash: string(encodedScopes),
}); err != nil {
return 0, fmt.Errorf("put langpack seed scopes for %q: %w", catalog, err)
}
manifest := seedCatalogSnapshot(seed)
encodedManifest, err := json.Marshal(manifest)
if err != nil {
return 0, fmt.Errorf("encode langpack seed catalog %q: %w", catalog, err)
}
if err := q.PutLangPackSeedHash(ctx, sqlcgen.PutLangPackSeedHashParams{
Key: langPackSeedManifestStateKey(catalog),
ContentHash: string(encodedManifest),
}); err != nil {
return 0, fmt.Errorf("put langpack seed catalog %q: %w", catalog, err)
}
return written, nil
}
func replacePackWithCopy(ctx context.Context, tx pgx.Tx, q *sqlcgen.Queries, pack domain.LangPack) error {
params := sqlcgen.DeleteLangPackStringsParams{LangPack: pack.LangPack, LangCode: pack.LangCode}
if err := q.DeleteLangPackStrings(ctx, params); err != nil {
return fmt.Errorf("delete previous lang pack strings: %w", err)
}
if err := q.UpsertLangPackMeta(ctx, sqlcgen.UpsertLangPackMetaParams{
LangPack: pack.LangPack,
LangCode: pack.LangCode,
Version: int32(pack.Version),
StringsCount: int32(len(pack.Strings)),
}); err != nil {
return fmt.Errorf("upsert lang pack meta: %w", err)
}
if len(pack.Strings) == 0 {
return nil
}
count, err := tx.CopyFrom(
ctx,
pgx.Identifier{"lang_pack_strings"},
[]string{
"lang_pack", "lang_code", "key", "version", "pluralized", "value",
"zero_value", "one_value", "two_value", "few_value", "many_value", "other_value", "deleted",
},
pgx.CopyFromSlice(len(pack.Strings), func(i int) ([]any, error) {
item := pack.Strings[i]
return []any{
pack.LangPack, pack.LangCode, item.Key, int32(pack.Version), item.Pluralized, item.Value,
item.ZeroValue, item.OneValue, item.TwoValue, item.FewValue, item.ManyValue, item.OtherValue, item.Deleted,
}, nil
}),
)
if err != nil {
return fmt.Errorf("copy lang pack strings: %w", err)
}
if count != int64(len(pack.Strings)) {
return fmt.Errorf("copy lang pack strings wrote %d rows, want %d", count, len(pack.Strings))
}
return nil
}
func replacePackWithQueries(ctx context.Context, q *sqlcgen.Queries, pack domain.LangPack) error {
if err := q.DeleteLangPackStrings(ctx, sqlcgen.DeleteLangPackStringsParams{LangPack: pack.LangPack, LangCode: pack.LangCode}); err != nil {
return fmt.Errorf("delete previous lang pack strings: %w", err)
}
if err := q.UpsertLangPackMeta(ctx, sqlcgen.UpsertLangPackMetaParams{
LangPack: pack.LangPack,
LangCode: pack.LangCode,
@ -120,6 +370,54 @@ func upsertPackWith(ctx context.Context, q *sqlcgen.Queries, pack domain.LangPac
return nil
}
func langPackSeedStateKey(langPack, langCode string) string {
return "langpack:v1:entry:" + langPack + ":" + langCode
}
func langPackSeedCatalogStateKey(catalog string) string {
return "langpack:v1:catalog:" + catalog
}
func langPackSeedManifestStateKey(catalog string) string {
return "langpack:v2:catalog:" + catalog
}
func validateSeedEntry(entry domain.LangPackSeedEntry) error {
if entry.SourceHash == "" || entry.ContentHash == "" || entry.StringsCount <= 0 {
return fmt.Errorf("langpack %s/%s has incomplete seed metadata", entry.Pack.LangPack, entry.Pack.LangCode)
}
if entry.ContentLoaded && len(entry.Pack.Strings) != entry.StringsCount {
return fmt.Errorf("langpack %s/%s loaded %d strings, want %d", entry.Pack.LangPack, entry.Pack.LangCode, len(entry.Pack.Strings), entry.StringsCount)
}
return nil
}
func seedCatalogSnapshot(seed domain.LangPackSeed) domain.LangPackSeedCatalog {
state := domain.LangPackSeedCatalog{
Catalog: seed.Catalog,
Scopes: append([]string(nil), seed.Scopes...),
Packs: make([]domain.LangPackSeedCatalogEntry, 0, len(seed.Packs)),
}
for _, entry := range seed.Packs {
state.Packs = append(state.Packs, domain.LangPackSeedCatalogEntry{
LangPack: entry.Pack.LangPack,
LangCode: entry.Pack.LangCode,
Version: entry.Pack.Version,
SourceHash: entry.SourceHash,
ContentHash: entry.ContentHash,
StringsCount: entry.StringsCount,
})
}
return state
}
func invalidateLangPackSeedCatalogs(ctx context.Context, db sqlcgen.DBTX) error {
if _, err := db.Exec(ctx, `DELETE FROM seed_states WHERE key LIKE 'langpack:v2:catalog:%'`); err != nil {
return fmt.Errorf("invalidate langpack seed catalogs: %w", err)
}
return nil
}
func (s *LangPackStore) meta(ctx context.Context, langPack, langCode string) (domain.LangPack, bool, error) {
row, err := s.q.GetLangPackMeta(ctx, sqlcgen.GetLangPackMetaParams{
LangPack: langPack,

View file

@ -0,0 +1,164 @@
package postgres
import (
"context"
"path/filepath"
"strings"
"testing"
"time"
applangpack "telesrv/internal/app/langpack"
"telesrv/internal/domain"
)
func TestLangPackSeedReconciliationPostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
packName := "seedtest-" + randomSuffix(t)
store := NewLangPackStore(pool)
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM lang_pack_strings WHERE lang_pack = $1", packName)
_, _ = pool.Exec(ctx, "DELETE FROM lang_packs WHERE lang_pack = $1", packName)
_, _ = pool.Exec(ctx, "DELETE FROM seed_states WHERE key LIKE $1 OR key = $2 OR key = $3", "langpack:v1:entry:"+packName+":%", "langpack:v1:catalog:"+packName, "langpack:v2:catalog:"+packName)
})
seedV1 := domain.LangPackSeed{
Catalog: packName,
Scopes: []string{packName},
Packs: []domain.LangPackSeedEntry{{
SourceHash: "source-v1",
ContentHash: "hash-v1",
StringsCount: 2,
ContentLoaded: true,
Pack: domain.LangPack{
LangPack: packName,
LangCode: "fr",
Version: 1,
Strings: []domain.LangPackString{
{Key: "LanguageName", Value: "Français"},
{Key: "old", Value: "old"},
},
},
}},
}
written, err := store.ReconcileSeed(ctx, seedV1)
if err != nil || written != 2 {
t.Fatalf("reconcile v1 = %d, %v", written, err)
}
seedV1Unloaded := seedV1
seedV1Unloaded.Packs = append([]domain.LangPackSeedEntry(nil), seedV1.Packs...)
seedV1Unloaded.Packs[0].Pack.Strings = nil
seedV1Unloaded.Packs[0].ContentLoaded = false
if written, err := store.ReconcileSeed(ctx, seedV1Unloaded); err != nil || written != 0 {
t.Fatalf("reconcile unchanged v1 = %d, %v", written, err)
}
catalog, err := store.GetSeedCatalog(ctx, packName)
if err != nil || len(catalog.Packs) != 1 || catalog.Packs[0].SourceHash != "source-v1" {
t.Fatalf("seed catalog = %+v, %v", catalog, err)
}
seedV2 := domain.LangPackSeed{
Catalog: packName,
Scopes: []string{packName},
Packs: []domain.LangPackSeedEntry{{
SourceHash: "source-v2",
ContentHash: "hash-v2",
StringsCount: 2,
ContentLoaded: true,
Pack: domain.LangPack{
LangPack: packName,
LangCode: "fr",
Version: 2,
Strings: []domain.LangPackString{
{Key: "LanguageName", Value: "Français v2"},
{Key: "new", Value: "new"},
},
},
}},
}
written, err = store.ReconcileSeed(ctx, seedV2)
if err != nil || written != 2 {
t.Fatalf("reconcile v2 = %d, %v", written, err)
}
pack, err := store.GetPack(ctx, packName, "fr", 0)
if err != nil || pack.Version != 2 || len(pack.Strings) != 2 || postgresLangPackValue(pack.Strings, "old") != "" || postgresLangPackValue(pack.Strings, "new") != "new" {
t.Fatalf("replaced postgres pack = %+v, err %v", pack, err)
}
mutated := seedV2
mutated.Packs = append([]domain.LangPackSeedEntry(nil), seedV2.Packs...)
mutated.Packs[0].ContentHash = "changed-without-version"
if _, err := store.ReconcileSeed(ctx, mutated); err == nil || !strings.Contains(err.Error(), "without version bump") {
t.Fatalf("same-version mutation error = %v", err)
}
rollback := seedV2
rollback.Packs = append([]domain.LangPackSeedEntry(nil), seedV2.Packs...)
rollback.Packs[0].Pack.Version = 1
rollback.Packs[0].ContentHash = "rollback"
if _, err := store.ReconcileSeed(ctx, rollback); err == nil || !strings.Contains(err.Error(), "version rollback") {
t.Fatalf("rollback error = %v", err)
}
if written, err := store.ReconcileSeed(ctx, domain.LangPackSeed{Catalog: packName}); err != nil || written != 0 {
t.Fatalf("remove missing language = %d, %v", written, err)
}
languages, err := store.ListLanguages(ctx, packName)
if err != nil || len(languages) != 0 {
t.Fatalf("languages after manifest removal = %+v, err %v", languages, err)
}
}
func postgresLangPackValue(items []domain.LangPackString, key string) string {
for _, item := range items {
if item.Key == key {
return item.Value
}
}
return ""
}
func TestBundledLangPackSeedPostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
scopes := []string{"android", "android_x", "ios", "macos", "tdesktop", "weba"}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM lang_pack_strings WHERE lang_pack = ANY($1::text[])", scopes)
_, _ = pool.Exec(ctx, "DELETE FROM lang_packs WHERE lang_pack = ANY($1::text[])", scopes)
for _, scope := range scopes {
_, _ = pool.Exec(ctx, "DELETE FROM seed_states WHERE key LIKE $1", "langpack:v1:entry:"+scope+":%")
}
_, _ = pool.Exec(ctx, "DELETE FROM seed_states WHERE key = 'langpack:v1:catalog:langpack'")
_, _ = pool.Exec(ctx, "DELETE FROM seed_states WHERE key = 'langpack:v2:catalog:langpack'")
})
service := applangpack.NewService(NewLangPackStore(pool))
root := filepath.Join("..", "..", "..", "data", "langpack")
started := time.Now()
seeded, err := service.SeedDirectory(ctx, root)
firstDuration := time.Since(started)
if err != nil {
t.Fatalf("seed bundled langpacks to postgres: %v", err)
}
if seeded < 50000 {
t.Fatalf("seeded bundled strings = %d, want full catalog", seeded)
}
started = time.Now()
seededAgain, err := service.SeedDirectory(ctx, root)
secondDuration := time.Since(started)
if err != nil || seededAgain != 0 {
t.Fatalf("reconcile unchanged bundled langpacks = %d, %v", seededAgain, err)
}
var metadataCount, activeCount int64
if err := pool.QueryRow(ctx, `
SELECT
(SELECT COALESCE(sum(strings_count), 0) FROM lang_packs WHERE lang_pack = ANY($1::text[])),
(SELECT count(*) FROM lang_pack_strings WHERE lang_pack = ANY($1::text[]) AND NOT deleted)
`, scopes).Scan(&metadataCount, &activeCount); err != nil {
t.Fatalf("count seeded langpack rows: %v", err)
}
if metadataCount != activeCount {
t.Fatalf("seeded strings_count = %d, active rows = %d", metadataCount, activeCount)
}
t.Logf("bundled langpack seed: first=%s unchanged=%s strings=%d", firstDuration, secondDuration, metadataCount)
}

View file

@ -30,6 +30,36 @@ ON CONFLICT (lang_pack, lang_code, key) DO UPDATE SET
deleted = EXCLUDED.deleted,
updated_at = now();
-- name: ListLangPackCodes :many
SELECT lang_code
FROM lang_packs
WHERE lang_pack = $1
ORDER BY lang_code;
-- name: DeleteLangPackStrings :exec
DELETE FROM lang_pack_strings
WHERE lang_pack = $1 AND lang_code = $2;
-- name: DeleteLangPackMeta :exec
DELETE FROM lang_packs
WHERE lang_pack = $1 AND lang_code = $2;
-- name: GetLangPackSeedHash :one
SELECT content_hash
FROM seed_states
WHERE key = $1;
-- name: PutLangPackSeedHash :exec
INSERT INTO seed_states (key, content_hash)
VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE SET
content_hash = EXCLUDED.content_hash,
updated_at = now();
-- name: DeleteLangPackSeedHash :exec
DELETE FROM seed_states
WHERE key = $1;
-- name: ListLangPackStrings :many
SELECT
lang_pack, lang_code, key, version, pluralized, value,
@ -45,3 +75,48 @@ SELECT
FROM lang_pack_strings
WHERE lang_pack = $1 AND lang_code = $2 AND key = ANY(sqlc.arg(keys)::text[]) AND NOT deleted
ORDER BY key;
-- name: ListLangPackLanguages :many
SELECT
p.lang_pack,
p.lang_code,
p.version,
p.strings_count,
COALESCE(
MAX(s.value) FILTER (WHERE s.key = 'LanguageNameInEnglish'),
MAX(s.value) FILTER (WHERE s.key = 'Localization.EnglishLanguageName'),
''
)::text AS name,
CASE
WHEN p.lang_pack = 'android' THEN COALESCE(
MAX(s.value) FILTER (WHERE s.key = 'TranslateLanguage' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
MAX(s.value) FILTER (WHERE s.key = 'PassportLanguage_' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
MAX(s.value) FILTER (WHERE s.key = 'LanguageName'),
''
)
ELSE COALESCE(
MAX(s.value) FILTER (WHERE s.key = 'lng_language_name'),
MAX(s.value) FILTER (WHERE s.key = 'LanguageName'),
MAX(s.value) FILTER (WHERE s.key = 'Localization.LanguageName'),
MAX(s.value) FILTER (WHERE s.key = 'TranslateLanguage' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
MAX(s.value) FILTER (WHERE s.key = 'PassportLanguage_' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
''
)
END::text AS native_name
FROM lang_packs p
LEFT JOIN lang_pack_strings s
ON s.lang_pack = p.lang_pack
AND s.lang_code = p.lang_code
AND s.key = ANY(ARRAY[
'lng_language_name',
'LanguageName',
'LanguageNameInEnglish',
'Localization.LanguageName',
'Localization.EnglishLanguageName',
'TranslateLanguage' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1)),
'PassportLanguage_' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))
]::text[])
AND NOT s.deleted
WHERE p.lang_pack = $1
GROUP BY p.lang_pack, p.lang_code, p.version, p.strings_count
ORDER BY p.lang_code;

View file

@ -9,6 +9,46 @@ import (
"context"
)
const deleteLangPackMeta = `-- name: DeleteLangPackMeta :exec
DELETE FROM lang_packs
WHERE lang_pack = $1 AND lang_code = $2
`
type DeleteLangPackMetaParams struct {
LangPack string
LangCode string
}
func (q *Queries) DeleteLangPackMeta(ctx context.Context, arg DeleteLangPackMetaParams) error {
_, err := q.db.Exec(ctx, deleteLangPackMeta, arg.LangPack, arg.LangCode)
return err
}
const deleteLangPackSeedHash = `-- name: DeleteLangPackSeedHash :exec
DELETE FROM seed_states
WHERE key = $1
`
func (q *Queries) DeleteLangPackSeedHash(ctx context.Context, key string) error {
_, err := q.db.Exec(ctx, deleteLangPackSeedHash, key)
return err
}
const deleteLangPackStrings = `-- name: DeleteLangPackStrings :exec
DELETE FROM lang_pack_strings
WHERE lang_pack = $1 AND lang_code = $2
`
type DeleteLangPackStringsParams struct {
LangPack string
LangCode string
}
func (q *Queries) DeleteLangPackStrings(ctx context.Context, arg DeleteLangPackStringsParams) error {
_, err := q.db.Exec(ctx, deleteLangPackStrings, arg.LangPack, arg.LangCode)
return err
}
const getLangPackMeta = `-- name: GetLangPackMeta :one
SELECT lang_pack, lang_code, version, strings_count
FROM lang_packs
@ -39,6 +79,19 @@ func (q *Queries) GetLangPackMeta(ctx context.Context, arg GetLangPackMetaParams
return i, err
}
const getLangPackSeedHash = `-- name: GetLangPackSeedHash :one
SELECT content_hash
FROM seed_states
WHERE key = $1
`
func (q *Queries) GetLangPackSeedHash(ctx context.Context, key string) (string, error) {
row := q.db.QueryRow(ctx, getLangPackSeedHash, key)
var content_hash string
err := row.Scan(&content_hash)
return content_hash, err
}
const getLangPackStringsByKeys = `-- name: GetLangPackStringsByKeys :many
SELECT
lang_pack, lang_code, key, version, pluralized, value,
@ -104,6 +157,115 @@ func (q *Queries) GetLangPackStringsByKeys(ctx context.Context, arg GetLangPackS
return items, nil
}
const listLangPackCodes = `-- name: ListLangPackCodes :many
SELECT lang_code
FROM lang_packs
WHERE lang_pack = $1
ORDER BY lang_code
`
func (q *Queries) ListLangPackCodes(ctx context.Context, langPack string) ([]string, error) {
rows, err := q.db.Query(ctx, listLangPackCodes, langPack)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var lang_code string
if err := rows.Scan(&lang_code); err != nil {
return nil, err
}
items = append(items, lang_code)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listLangPackLanguages = `-- name: ListLangPackLanguages :many
SELECT
p.lang_pack,
p.lang_code,
p.version,
p.strings_count,
COALESCE(
MAX(s.value) FILTER (WHERE s.key = 'LanguageNameInEnglish'),
MAX(s.value) FILTER (WHERE s.key = 'Localization.EnglishLanguageName'),
''
)::text AS name,
CASE
WHEN p.lang_pack = 'android' THEN COALESCE(
MAX(s.value) FILTER (WHERE s.key = 'TranslateLanguage' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
MAX(s.value) FILTER (WHERE s.key = 'PassportLanguage_' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
MAX(s.value) FILTER (WHERE s.key = 'LanguageName'),
''
)
ELSE COALESCE(
MAX(s.value) FILTER (WHERE s.key = 'lng_language_name'),
MAX(s.value) FILTER (WHERE s.key = 'LanguageName'),
MAX(s.value) FILTER (WHERE s.key = 'Localization.LanguageName'),
MAX(s.value) FILTER (WHERE s.key = 'TranslateLanguage' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
MAX(s.value) FILTER (WHERE s.key = 'PassportLanguage_' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
''
)
END::text AS native_name
FROM lang_packs p
LEFT JOIN lang_pack_strings s
ON s.lang_pack = p.lang_pack
AND s.lang_code = p.lang_code
AND s.key = ANY(ARRAY[
'lng_language_name',
'LanguageName',
'LanguageNameInEnglish',
'Localization.LanguageName',
'Localization.EnglishLanguageName',
'TranslateLanguage' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1)),
'PassportLanguage_' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))
]::text[])
AND NOT s.deleted
WHERE p.lang_pack = $1
GROUP BY p.lang_pack, p.lang_code, p.version, p.strings_count
ORDER BY p.lang_code
`
type ListLangPackLanguagesRow struct {
LangPack string
LangCode string
Version int32
StringsCount int32
Name string
NativeName string
}
func (q *Queries) ListLangPackLanguages(ctx context.Context, langPack string) ([]ListLangPackLanguagesRow, error) {
rows, err := q.db.Query(ctx, listLangPackLanguages, langPack)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListLangPackLanguagesRow
for rows.Next() {
var i ListLangPackLanguagesRow
if err := rows.Scan(
&i.LangPack,
&i.LangCode,
&i.Version,
&i.StringsCount,
&i.Name,
&i.NativeName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listLangPackStrings = `-- name: ListLangPackStrings :many
SELECT
lang_pack, lang_code, key, version, pluralized, value,
@ -168,6 +330,24 @@ func (q *Queries) ListLangPackStrings(ctx context.Context, arg ListLangPackStrin
return items, nil
}
const putLangPackSeedHash = `-- name: PutLangPackSeedHash :exec
INSERT INTO seed_states (key, content_hash)
VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE SET
content_hash = EXCLUDED.content_hash,
updated_at = now()
`
type PutLangPackSeedHashParams struct {
Key string
ContentHash string
}
func (q *Queries) PutLangPackSeedHash(ctx context.Context, arg PutLangPackSeedHashParams) error {
_, err := q.db.Exec(ctx, putLangPackSeedHash, arg.Key, arg.ContentHash)
return err
}
const upsertLangPackMeta = `-- name: UpsertLangPackMeta :exec
INSERT INTO lang_packs (lang_pack, lang_code, version, strings_count)
VALUES ($1, $2, $3, $4)