added full internationalization for the plugin

This commit is contained in:
Дмитрий Пиченикин 2026-02-16 13:50:43 +03:00
parent 16cb9a3d56
commit b975b5f5f1
30 changed files with 1282 additions and 271 deletions

68
go.mod
View File

@ -1,13 +1,77 @@
module github.com/larkox/mattermost-plugin-badges module github.com/larkox/mattermost-plugin-badges
go 1.12 go 1.24.0
toolchain go1.24.3
require ( require (
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/mattermost/mattermost-plugin-api v0.0.14 github.com/mattermost/mattermost-plugin-api v0.0.14
github.com/mattermost/mattermost-server/v5 v5.3.2-0.20210422214809-ff657bfdef24 github.com/mattermost/mattermost-server/v5 v5.3.2-0.20210422214809-ff657bfdef24
github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect golang.org/x/text v0.34.0
)
require (
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/dyatlov/go-opengraph v0.0.0-20210112100619-dae8665a5b09 // indirect
github.com/fatih/color v1.10.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/golang/protobuf v1.5.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-hclog v0.15.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.4.0 // indirect
github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 // indirect
github.com/json-iterator/go v1.1.10 // indirect
github.com/klauspost/cpuid/v2 v2.0.5 // indirect
github.com/lib/pq v1.10.0 // indirect
github.com/mattermost/go-i18n v1.11.0 // indirect
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d // indirect
github.com/mattermost/logr v1.0.13 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.10 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml v1.8.1 // indirect
github.com/philhofer/fwd v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/xid v1.2.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/tinylib/msgp v1.1.5 // indirect
github.com/wiggin77/cfg v1.0.2 // indirect
github.com/wiggin77/merror v1.0.3 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.16.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.40.0 // indirect
google.golang.org/genproto v0.0.0-20210322173543-5f0e89347f5a // indirect
google.golang.org/grpc v1.36.0 // indirect
google.golang.org/protobuf v1.26.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

40
go.sum
View File

@ -50,8 +50,9 @@ github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ClickHouse/clickhouse-go v1.3.12/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= github.com/ClickHouse/clickhouse-go v1.3.12/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
@ -279,7 +280,6 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gorp/gorp v2.0.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E= github.com/go-gorp/gorp v2.0.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E=
github.com/go-gorp/gorp v2.2.0+incompatible h1:xAUh4QgEeqPPhK3vxZN+bzrim1z5Av6q837gtjUlshc=
github.com/go-gorp/gorp v2.2.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E= github.com/go-gorp/gorp v2.2.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -362,8 +362,9 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -550,7 +551,6 @@ github.com/klauspost/compress v1.11.12/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@ -639,7 +639,6 @@ github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -700,12 +699,13 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nelsam/hel/v2 v2.3.2 h1:tXRsJBqRxj4ISSPCrXhbqF8sT+BXA/UaIvjhYjP5Bhk=
github.com/nelsam/hel/v2 v2.3.2/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w= github.com/nelsam/hel/v2 v2.3.2/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w=
github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM=
github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo= github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo=
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
@ -785,7 +785,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/poy/onpar v0.0.0-20200406201722-06f95a1c68e8/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= github.com/poy/onpar v0.0.0-20200406201722-06f95a1c68e8/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU=
github.com/poy/onpar v1.0.0 h1:MfdQ9bnas+J1si8vUHAABXKxqOqDVaH4T3LRDYYv5Lo=
github.com/poy/onpar v1.0.0/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/poy/onpar v1.0.0/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@ -1013,7 +1012,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@ -1058,6 +1056,8 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/build v0.0.0-20190314133821-5284462c4bec/go.mod h1:atTaCNAy0f16Ah5aV1gMSwgiKVHwu/JncqDpuRr7lS4= golang.org/x/build v0.0.0-20190314133821-5284462c4bec/go.mod h1:atTaCNAy0f16Ah5aV1gMSwgiKVHwu/JncqDpuRr7lS4=
@ -1081,8 +1081,9 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -1125,8 +1126,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1182,8 +1184,9 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -1206,6 +1209,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1276,8 +1281,9 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1286,8 +1292,9 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1357,14 +1364,14 @@ golang.org/x/tools v0.0.0-20200818005847-188abfa75333/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
@ -1521,8 +1528,9 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -1,7 +1,7 @@
{ {
"id": "ru.loop.plugin.achievements", "id": "ru.loop.plugin.achievements",
"name": "Badges for Mattermost", "name": "Achievements",
"description": "This plugin add badges support to Mattermost.", "description": "Плагин достижений и значков для Loop.",
"homepage_url": "https://github.com/larkox/mattermost-plugin-badges", "homepage_url": "https://github.com/larkox/mattermost-plugin-badges",
"support_url": "https://github.com/larkox/mattermost-plugin-badges/issues", "support_url": "https://github.com/larkox/mattermost-plugin-badges/issues",
"release_notes_url": "https://github.com/larkox/mattermost-plugin-badges/releases/tag/v0.2.1", "release_notes_url": "https://github.com/larkox/mattermost-plugin-badges/releases/tag/v0.2.1",
@ -24,9 +24,8 @@
"settings": [ "settings": [
{ {
"key": "BadgesAdmin", "key": "BadgesAdmin",
"display_name": "Badges admin:", "display_name": "Achievements Admin:",
"type": "text", "type": "custom"
"help_text": "This user will be considered as an admin for the badges plugin. They can create types, and modify and grant any badge."
} }
] ]
} }

View File

@ -2,7 +2,6 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"runtime/debug" "runtime/debug"
"strings" "strings"
@ -95,15 +94,18 @@ func dialogKeepOpen(w http.ResponseWriter) {
func (p *Plugin) dialogCreateBadge(w http.ResponseWriter, r *http.Request, userID string) { func (p *Plugin) dialogCreateBadge(w http.ResponseWriter, r *http.Request, userID string) {
req := model.SubmitDialogRequestFromJson(r.Body) req := model.SubmitDialogRequestFromJson(r.Body)
if req == nil { if req == nil {
dialogError(w, "could not get the dialog request", nil) T := p.getT("ru")
dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil)
return return
} }
user, err := p.mm.User.Get(userID) user, err := p.mm.User.Get(userID)
if err != nil { if err != nil {
dialogError(w, "could not get the user", nil) T := p.getT("ru")
dialogError(w, T("badges.api.cannot_get_user", "Не удалось найти пользователя"), nil)
return return
} }
T := p.getT(user.Locale)
toCreate := &badgesmodel.Badge{} toCreate := &badgesmodel.Badge{}
toCreate.CreatedBy = userID toCreate.CreatedBy = userID
@ -132,7 +134,7 @@ func (p *Plugin) dialogCreateBadge(w http.ResponseWriter, r *http.Request, userI
image = image[1 : len(image)-1] image = image[1 : len(image)-1]
} }
if image == "" { if image == "" {
dialogError(w, "Invalid field", map[string]string{"image": "Empty emoji"}) dialogError(w, T("badges.api.invalid_field", "Некорректное поле"), map[string]string{"image": T("badges.api.empty_emoji", "Пустой эмодзи")})
return return
} }
toCreate.Image = image toCreate.Image = image
@ -148,12 +150,12 @@ func (p *Plugin) dialogCreateBadge(w http.ResponseWriter, r *http.Request, userI
t, err := p.store.GetType(badgesmodel.BadgeType(badgeTypeStr)) t, err := p.store.GetType(badgesmodel.BadgeType(badgeTypeStr))
if err != nil { if err != nil {
dialogError(w, "this type does not exist", nil) dialogError(w, T("badges.api.type_not_exist", "Этот тип не существует"), nil)
return return
} }
if !canCreateBadge(user, p.badgeAdminUserID, t) { if !canCreateBadge(user, p.badgeAdminUserID, t) {
dialogError(w, "you have no permissions to create this badge", nil) dialogError(w, T("badges.api.no_permissions_create_badge", "У вас нет прав на создание этого значка"), nil)
return return
} }
@ -166,7 +168,7 @@ func (p *Plugin) dialogCreateBadge(w http.ResponseWriter, r *http.Request, userI
p.mm.Post.SendEphemeralPost(userID, &model.Post{ p.mm.Post.SendEphemeralPost(userID, &model.Post{
UserId: p.BotUserID, UserId: p.BotUserID,
ChannelId: req.ChannelId, ChannelId: req.ChannelId,
Message: fmt.Sprintf("Badge `%s` created.", toCreate.Name), Message: T("badges.api.badge_created", "Значок `%s` создан.", toCreate.Name),
}) })
dialogOK(w) dialogOK(w)
@ -175,18 +177,21 @@ func (p *Plugin) dialogCreateBadge(w http.ResponseWriter, r *http.Request, userI
func (p *Plugin) dialogCreateType(w http.ResponseWriter, r *http.Request, userID string) { func (p *Plugin) dialogCreateType(w http.ResponseWriter, r *http.Request, userID string) {
req := model.SubmitDialogRequestFromJson(r.Body) req := model.SubmitDialogRequestFromJson(r.Body)
if req == nil { if req == nil {
dialogError(w, "could not get the dialog request", nil) T := p.getT("ru")
dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil)
return return
} }
u, err := p.mm.User.Get(userID) u, err := p.mm.User.Get(userID)
if err != nil { if err != nil {
dialogError(w, "cannot get user", nil) T := p.getT("ru")
dialogError(w, T("badges.api.cannot_get_user", "Не удалось найти пользователя"), nil)
return return
} }
T := p.getT(u.Locale)
if !canCreateType(u, p.badgeAdminUserID, false) { if !canCreateType(u, p.badgeAdminUserID, false) {
dialogError(w, "you have no permissions to create a type", nil) dialogError(w, T("badges.api.no_permissions_create_type", "У вас нет прав на создание типа"), nil)
return return
} }
@ -212,29 +217,29 @@ func (p *Plugin) dialogCreateType(w http.ResponseWriter, r *http.Request, userID
if username == "" { if username == "" {
continue continue
} }
u, err := p.mm.User.GetByUsername(username) foundUser, userErr := p.mm.User.GetByUsername(username)
if err != nil { if userErr != nil {
dialogError(w, "Cannot find user", map[string]string{DialogFieldTypeAllowlistCanCreate: fmt.Sprintf("Error getting user %s. Error: %v", username, err)}) dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), map[string]string{DialogFieldTypeAllowlistCanCreate: T("badges.api.error_getting_user", "Ошибка получения пользователя %s: %v", username, userErr)})
return return
} }
toCreate.CanCreate.AllowList[u.Id] = true toCreate.CanCreate.AllowList[foundUser.Id] = true
} }
} }
if grantAllowList != "" { if grantAllowList != "" {
toCreate.CanGrant.AllowList = map[string]bool{} toCreate.CanGrant.AllowList = map[string]bool{}
usernames := strings.Split(createAllowList, ",") usernames := strings.Split(grantAllowList, ",")
for _, username := range usernames { for _, username := range usernames {
username = strings.TrimSpace(username) username = strings.TrimSpace(username)
if username == "" { if username == "" {
continue continue
} }
u, err := p.mm.User.GetByUsername(username) foundUser, userErr := p.mm.User.GetByUsername(username)
if err != nil { if userErr != nil {
dialogError(w, "Cannot find user", map[string]string{DialogFieldTypeAllowlistCanGrant: fmt.Sprintf("Error getting user %s. Error: %v", username, err)}) dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), map[string]string{DialogFieldTypeAllowlistCanGrant: T("badges.api.error_getting_user", "Ошибка получения пользователя %s: %v", username, userErr)})
return return
} }
toCreate.CanGrant.AllowList[u.Id] = true toCreate.CanGrant.AllowList[foundUser.Id] = true
} }
} }
@ -247,7 +252,7 @@ func (p *Plugin) dialogCreateType(w http.ResponseWriter, r *http.Request, userID
p.mm.Post.SendEphemeralPost(userID, &model.Post{ p.mm.Post.SendEphemeralPost(userID, &model.Post{
UserId: p.BotUserID, UserId: p.BotUserID,
ChannelId: req.ChannelId, ChannelId: req.ChannelId,
Message: fmt.Sprintf("Type `%s` created.", toCreate.Name), Message: T("badges.api.type_created", "Тип `%s` создан.", toCreate.Name),
}) })
dialogOK(w) dialogOK(w)
@ -257,7 +262,8 @@ func (p *Plugin) dialogCreateType(w http.ResponseWriter, r *http.Request, userID
func (p *Plugin) dialogSelectType(w http.ResponseWriter, r *http.Request, userID string) { func (p *Plugin) dialogSelectType(w http.ResponseWriter, r *http.Request, userID string) {
req := model.SubmitDialogRequestFromJson(r.Body) req := model.SubmitDialogRequestFromJson(r.Body)
if req == nil { if req == nil {
dialogError(w, "could not get the dialog request", nil) T := p.getT("ru")
dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil)
return return
} }
@ -269,18 +275,21 @@ func (p *Plugin) dialogSelectType(w http.ResponseWriter, r *http.Request, userID
t, err := p.store.GetType(badgesmodel.BadgeType(badgeTypeStr)) t, err := p.store.GetType(badgesmodel.BadgeType(badgeTypeStr))
if err != nil { if err != nil {
dialogError(w, "Cannot get type", map[string]string{DialogFieldBadgeType: "cannot get type"}) T := p.getT("ru")
dialogError(w, T("badges.api.cannot_get_type", "Не удалось получить тип"), map[string]string{DialogFieldBadgeType: T("badges.api.cannot_get_type", "Не удалось получить тип")})
return return
} }
u, err := p.mm.User.Get(userID) u, err := p.mm.User.Get(userID)
if err != nil { if err != nil {
dialogError(w, "Cannot find user", nil) T := p.getT("ru")
dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), nil)
return return
} }
T := p.getT(u.Locale)
if !canEditType(u, p.badgeAdminUserID, t) { if !canEditType(u, p.badgeAdminUserID, t) {
dialogError(w, "You cannot edit this type", nil) dialogError(w, T("badges.api.cannot_edit_type", "Вы не можете редактировать этот тип"), nil)
return return
} }
@ -297,26 +306,29 @@ func (p *Plugin) dialogSelectType(w http.ResponseWriter, r *http.Request, userID
func (p *Plugin) dialogEditType(w http.ResponseWriter, r *http.Request, userID string) { func (p *Plugin) dialogEditType(w http.ResponseWriter, r *http.Request, userID string) {
req := model.SubmitDialogRequestFromJson(r.Body) req := model.SubmitDialogRequestFromJson(r.Body)
if req == nil { if req == nil {
dialogError(w, "could not get the dialog request", nil) T := p.getT("ru")
dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil)
return return
} }
u, err := p.mm.User.Get(userID) u, err := p.mm.User.Get(userID)
if err != nil { if err != nil {
dialogError(w, "Cannot find user", nil) T := p.getT("ru")
dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), nil)
return return
} }
T := p.getT(u.Locale)
originalTypeID := req.State originalTypeID := req.State
originalType, err := p.store.GetType(badgesmodel.BadgeType(originalTypeID)) originalType, err := p.store.GetType(badgesmodel.BadgeType(originalTypeID))
if err != nil { if err != nil {
dialogError(w, "could not get the type", nil) dialogError(w, T("badges.api.could_not_get_type", "Не удалось получить тип"), nil)
return return
} }
if !canEditType(u, p.badgeAdminUserID, originalType) { if !canEditType(u, p.badgeAdminUserID, originalType) {
dialogError(w, "you have no permissions to edit this type", nil) dialogError(w, T("badges.api.no_permissions_edit_type", "У вас нет прав на редактирование этого типа"), nil)
return return
} }
@ -349,7 +361,7 @@ func (p *Plugin) dialogEditType(w http.ResponseWriter, r *http.Request, userID s
var allowedUser *model.User var allowedUser *model.User
allowedUser, err = p.mm.User.GetByUsername(username) allowedUser, err = p.mm.User.GetByUsername(username)
if err != nil { if err != nil {
dialogError(w, "Cannot find user", map[string]string{DialogFieldTypeAllowlistCanCreate: fmt.Sprintf("Error getting user %s. Error: %v", username, err)}) dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), map[string]string{DialogFieldTypeAllowlistCanCreate: T("badges.api.error_getting_user", "Ошибка получения пользователя %s: %v", username, err)})
return return
} }
originalType.CanCreate.AllowList[allowedUser.Id] = true originalType.CanCreate.AllowList[allowedUser.Id] = true
@ -365,7 +377,7 @@ func (p *Plugin) dialogEditType(w http.ResponseWriter, r *http.Request, userID s
var allowedUser *model.User var allowedUser *model.User
allowedUser, err = p.mm.User.GetByUsername(username) allowedUser, err = p.mm.User.GetByUsername(username)
if err != nil { if err != nil {
dialogError(w, "Cannot find user", map[string]string{DialogFieldTypeAllowlistCanGrant: fmt.Sprintf("Error getting user %s. Error: %v", username, err)}) dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), map[string]string{DialogFieldTypeAllowlistCanGrant: T("badges.api.error_getting_user", "Ошибка получения пользователя %s: %v", username, err)})
return return
} }
originalType.CanGrant.AllowList[allowedUser.Id] = true originalType.CanGrant.AllowList[allowedUser.Id] = true
@ -380,7 +392,7 @@ func (p *Plugin) dialogEditType(w http.ResponseWriter, r *http.Request, userID s
p.mm.Post.SendEphemeralPost(userID, &model.Post{ p.mm.Post.SendEphemeralPost(userID, &model.Post{
UserId: p.BotUserID, UserId: p.BotUserID,
ChannelId: req.ChannelId, ChannelId: req.ChannelId,
Message: fmt.Sprintf("Type `%s` updated.", originalType.Name), Message: T("badges.api.type_updated", "Тип `%s` обновлён.", originalType.Name),
}) })
dialogOK(w) dialogOK(w)
@ -390,7 +402,8 @@ func (p *Plugin) dialogEditType(w http.ResponseWriter, r *http.Request, userID s
func (p *Plugin) dialogSelectBadge(w http.ResponseWriter, r *http.Request, userID string) { func (p *Plugin) dialogSelectBadge(w http.ResponseWriter, r *http.Request, userID string) {
req := model.SubmitDialogRequestFromJson(r.Body) req := model.SubmitDialogRequestFromJson(r.Body)
if req == nil { if req == nil {
dialogError(w, "could not get the dialog request", nil) T := p.getT("ru")
dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil)
return return
} }
@ -402,18 +415,21 @@ func (p *Plugin) dialogSelectBadge(w http.ResponseWriter, r *http.Request, userI
b, err := p.store.GetBadge(badgesmodel.BadgeID(badgeIDStr)) b, err := p.store.GetBadge(badgesmodel.BadgeID(badgeIDStr))
if err != nil { if err != nil {
dialogError(w, "Cannot get type", map[string]string{DialogFieldBadge: "cannot get badge"}) T := p.getT("ru")
dialogError(w, T("badges.api.cannot_get_badge", "Не удалось получить значок"), map[string]string{DialogFieldBadge: T("badges.api.cannot_get_badge", "Не удалось получить значок")})
return return
} }
u, err := p.mm.User.Get(userID) u, err := p.mm.User.Get(userID)
if err != nil { if err != nil {
dialogError(w, "Cannot find user", nil) T := p.getT("ru")
dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), nil)
return return
} }
T := p.getT(u.Locale)
if !canEditBadge(u, p.badgeAdminUserID, b) { if !canEditBadge(u, p.badgeAdminUserID, b) {
dialogError(w, "You cannot edit this badge", nil) dialogError(w, T("badges.api.cannot_edit_badge", "Вы не можете редактировать этот значок"), nil)
return return
} }
@ -430,26 +446,29 @@ func (p *Plugin) dialogSelectBadge(w http.ResponseWriter, r *http.Request, userI
func (p *Plugin) dialogEditBadge(w http.ResponseWriter, r *http.Request, userID string) { func (p *Plugin) dialogEditBadge(w http.ResponseWriter, r *http.Request, userID string) {
req := model.SubmitDialogRequestFromJson(r.Body) req := model.SubmitDialogRequestFromJson(r.Body)
if req == nil { if req == nil {
dialogError(w, "could not get the dialog request", nil) T := p.getT("ru")
dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil)
return return
} }
u, err := p.mm.User.Get(userID) u, err := p.mm.User.Get(userID)
if err != nil { if err != nil {
dialogError(w, "Cannot find user", nil) T := p.getT("ru")
dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), nil)
return return
} }
T := p.getT(u.Locale)
originalBadgeID := req.State originalBadgeID := req.State
originalBadge, err := p.store.GetBadge(badgesmodel.BadgeID(originalBadgeID)) originalBadge, err := p.store.GetBadge(badgesmodel.BadgeID(originalBadgeID))
if err != nil { if err != nil {
dialogError(w, "could not get the badge", nil) dialogError(w, T("badges.api.could_not_get_badge", "Не удалось получить значок"), nil)
return return
} }
if !canEditBadge(u, p.badgeAdminUserID, originalBadge) { if !canEditBadge(u, p.badgeAdminUserID, originalBadge) {
dialogError(w, "you have no permissions to edit this type", nil) dialogError(w, T("badges.api.no_permissions_edit_badge", "У вас нет прав на редактирование этого значка"), nil)
return return
} }
@ -485,7 +504,7 @@ func (p *Plugin) dialogEditBadge(w http.ResponseWriter, r *http.Request, userID
image = image[1 : len(image)-1] image = image[1 : len(image)-1]
} }
if image == "" { if image == "" {
dialogError(w, "Invalid field", map[string]string{"image": "Empty emoji"}) dialogError(w, T("badges.api.invalid_field", "Некорректное поле"), map[string]string{"image": T("badges.api.empty_emoji", "Пустой эмодзи")})
return return
} }
originalBadge.Image = image originalBadge.Image = image
@ -509,7 +528,7 @@ func (p *Plugin) dialogEditBadge(w http.ResponseWriter, r *http.Request, userID
p.mm.Post.SendEphemeralPost(userID, &model.Post{ p.mm.Post.SendEphemeralPost(userID, &model.Post{
UserId: p.BotUserID, UserId: p.BotUserID,
ChannelId: req.ChannelId, ChannelId: req.ChannelId,
Message: fmt.Sprintf("Badge `%s` updated.", originalBadge.Name), Message: T("badges.api.badge_updated", "Значок `%s` обновлён.", originalBadge.Name),
}) })
dialogOK(w) dialogOK(w)
@ -518,7 +537,8 @@ func (p *Plugin) dialogEditBadge(w http.ResponseWriter, r *http.Request, userID
func (p *Plugin) dialogGrant(w http.ResponseWriter, r *http.Request, userID string) { func (p *Plugin) dialogGrant(w http.ResponseWriter, r *http.Request, userID string) {
req := model.SubmitDialogRequestFromJson(r.Body) req := model.SubmitDialogRequestFromJson(r.Body)
if req == nil { if req == nil {
dialogError(w, "could not get the dialog request", nil) T := p.getT("ru")
dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil)
return return
} }
@ -532,7 +552,8 @@ func (p *Plugin) dialogGrant(w http.ResponseWriter, r *http.Request, userID stri
badge, err := p.store.GetBadge(badgesmodel.BadgeID(badgeIDStr)) badge, err := p.store.GetBadge(badgesmodel.BadgeID(badgeIDStr))
if err != nil { if err != nil {
dialogError(w, "badge not found", nil) T := p.getT("ru")
dialogError(w, T("badges.api.badge_not_found", "Значок не найден"), nil)
return return
} }
@ -541,6 +562,7 @@ func (p *Plugin) dialogGrant(w http.ResponseWriter, r *http.Request, userID stri
dialogError(w, err.Error(), nil) dialogError(w, err.Error(), nil)
return return
} }
T := p.getT(granter.Locale)
badgeType, err := p.store.GetType(badge.Type) badgeType, err := p.store.GetType(badge.Type)
if err != nil { if err != nil {
@ -549,7 +571,7 @@ func (p *Plugin) dialogGrant(w http.ResponseWriter, r *http.Request, userID stri
} }
if !canGrantBadge(granter, p.badgeAdminUserID, badge, badgeType) { if !canGrantBadge(granter, p.badgeAdminUserID, badge, badgeType) {
dialogError(w, "you have no permissions to grant this badge", nil) dialogError(w, T("badges.api.no_permissions_grant", "У вас нет прав на выдачу этого значка"), nil)
return return
} }
@ -564,7 +586,7 @@ func (p *Plugin) dialogGrant(w http.ResponseWriter, r *http.Request, userID stri
grantToUser, err := p.mm.User.Get(grantToID) grantToUser, err := p.mm.User.Get(grantToID)
if err != nil { if err != nil {
dialogError(w, "user not found", nil) dialogError(w, T("badges.api.user_not_found", "Пользователь не найден"), nil)
return return
} }
@ -587,7 +609,7 @@ func (p *Plugin) dialogGrant(w http.ResponseWriter, r *http.Request, userID stri
p.mm.Post.SendEphemeralPost(userID, &model.Post{ p.mm.Post.SendEphemeralPost(userID, &model.Post{
UserId: p.BotUserID, UserId: p.BotUserID,
ChannelId: req.ChannelId, ChannelId: req.ChannelId,
Message: fmt.Sprintf("Badge `%s` granted to @%s.", badge.Name, grantToUser.Username), Message: T("badges.api.badge_granted", "Значок `%s` выдан @%s.", badge.Name, grantToUser.Username),
}) })
dialogOK(w) dialogOK(w)
@ -596,7 +618,8 @@ func (p *Plugin) dialogGrant(w http.ResponseWriter, r *http.Request, userID stri
func (p *Plugin) dialogCreateSubscription(w http.ResponseWriter, r *http.Request, userID string) { func (p *Plugin) dialogCreateSubscription(w http.ResponseWriter, r *http.Request, userID string) {
req := model.SubmitDialogRequestFromJson(r.Body) req := model.SubmitDialogRequestFromJson(r.Body)
if req == nil { if req == nil {
dialogError(w, "could not get the dialog request", nil) T := p.getT("ru")
dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil)
return return
} }
@ -605,9 +628,10 @@ func (p *Plugin) dialogCreateSubscription(w http.ResponseWriter, r *http.Request
dialogError(w, err.Error(), nil) dialogError(w, err.Error(), nil)
return return
} }
T := p.getT(u.Locale)
if !canCreateSubscription(u, p.badgeAdminUserID, req.ChannelId) { if !canCreateSubscription(u, p.badgeAdminUserID, req.ChannelId) {
dialogError(w, "You cannot create a subscription", nil) dialogError(w, T("badges.api.cannot_create_subscription", "Вы не можете создать подписку"), nil)
return return
} }
@ -625,7 +649,7 @@ func (p *Plugin) dialogCreateSubscription(w http.ResponseWriter, r *http.Request
p.mm.Post.SendEphemeralPost(userID, &model.Post{ p.mm.Post.SendEphemeralPost(userID, &model.Post{
UserId: p.BotUserID, UserId: p.BotUserID,
ChannelId: req.ChannelId, ChannelId: req.ChannelId,
Message: "Subscription added", Message: T("badges.api.subscription_added", "Подписка добавлена"),
}) })
dialogOK(w) dialogOK(w)
@ -634,7 +658,8 @@ func (p *Plugin) dialogCreateSubscription(w http.ResponseWriter, r *http.Request
func (p *Plugin) dialogDeleteSubscription(w http.ResponseWriter, r *http.Request, userID string) { func (p *Plugin) dialogDeleteSubscription(w http.ResponseWriter, r *http.Request, userID string) {
req := model.SubmitDialogRequestFromJson(r.Body) req := model.SubmitDialogRequestFromJson(r.Body)
if req == nil { if req == nil {
dialogError(w, "could not get the dialog request", nil) T := p.getT("ru")
dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil)
return return
} }
@ -643,9 +668,10 @@ func (p *Plugin) dialogDeleteSubscription(w http.ResponseWriter, r *http.Request
dialogError(w, err.Error(), nil) dialogError(w, err.Error(), nil)
return return
} }
T := p.getT(u.Locale)
if !canCreateSubscription(u, p.badgeAdminUserID, req.ChannelId) { if !canCreateSubscription(u, p.badgeAdminUserID, req.ChannelId) {
dialogError(w, "You cannot delete a subscription", nil) dialogError(w, T("badges.api.cannot_delete_subscription", "Вы не можете удалить подписку"), nil)
return return
} }
@ -663,7 +689,7 @@ func (p *Plugin) dialogDeleteSubscription(w http.ResponseWriter, r *http.Request
p.mm.Post.SendEphemeralPost(userID, &model.Post{ p.mm.Post.SendEphemeralPost(userID, &model.Post{
UserId: p.BotUserID, UserId: p.BotUserID,
ChannelId: req.ChannelId, ChannelId: req.ChannelId,
Message: "Subscription removed", Message: T("badges.api.subscription_removed", "Подписка удалена"),
}) })
dialogOK(w) dialogOK(w)
@ -967,13 +993,15 @@ func (p *Plugin) extractUserMiddleWare(handler HTTPHandlerFuncWithUser, response
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID") userID := r.Header.Get("Mattermost-User-ID")
if userID == "" { if userID == "" {
T := p.getT("ru")
msg := T("badges.api.not_authorized", "Не авторизован")
switch responseType { switch responseType {
case ResponseTypeJSON: case ResponseTypeJSON:
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: "Not authorized.", StatusCode: http.StatusUnauthorized}) p.writeAPIError(w, &APIErrorResponse{ID: "", Message: msg, StatusCode: http.StatusUnauthorized})
case ResponseTypePlain: case ResponseTypePlain:
http.Error(w, "Not authorized", http.StatusUnauthorized) http.Error(w, msg, http.StatusUnauthorized)
case ResponseTypeDialog: case ResponseTypeDialog:
dialogError(w, "Not Authorized", nil) dialogError(w, msg, nil)
default: default:
p.mm.Log.Error("Unknown ResponseType detected") p.mm.Log.Error("Unknown ResponseType detected")
} }

View File

@ -19,8 +19,8 @@ func getHelp() string {
func (p *Plugin) getCommand() *model.Command { func (p *Plugin) getCommand() *model.Command {
return &model.Command{ return &model.Command{
Trigger: "badges", Trigger: "badges",
DisplayName: "Badges Bot", DisplayName: "Achievements Bot",
Description: "Badges", Description: "Achievements",
AutoComplete: true, AutoComplete: true,
AutoCompleteDesc: "Available commands:", AutoCompleteDesc: "Available commands:",
AutoCompleteHint: "[command]", AutoCompleteHint: "[command]",
@ -77,7 +77,13 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo
p.postCommandResponse(args, fmt.Sprintf("__Error: %s__", err.Error())) p.postCommandResponse(args, fmt.Sprintf("__Error: %s__", err.Error()))
} else { } else {
p.mm.Log.Error(err.Error()) p.mm.Log.Error(err.Error())
p.postCommandResponse(args, "An unknown error occurred. Please talk to your system administrator for help.") u, _ := p.mm.User.Get(args.UserId)
locale := "ru"
if u != nil {
locale = u.Locale
}
T := p.getT(locale)
p.postCommandResponse(args, T("badges.error.unknown", "Произошла неизвестная ошибка. Обратитесь к системному администратору."))
} }
} }
@ -93,11 +99,12 @@ func (p *Plugin) runClean(args []string, extra *model.CommandArgs) (bool, *model
if err != nil { if err != nil {
return false, &model.CommandResponse{Text: "Cannot get user."}, nil return false, &model.CommandResponse{Text: "Cannot get user."}, nil
} }
T := p.getT(user.Locale)
if !user.IsSystemAdmin() { if !user.IsSystemAdmin() {
return false, &model.CommandResponse{Text: "Only a system admin can clean the badges database."}, nil return false, &model.CommandResponse{Text: T("badges.error.only_sysadmin_clean", "Только системный администратор может очистить базу значков.")}, nil
} }
_ = p.mm.KV.DeleteAll() _ = p.mm.KV.DeleteAll()
return false, &model.CommandResponse{Text: "Clean"}, nil return false, &model.CommandResponse{Text: T("badges.success.clean", "Очищено")}, nil
} }
func (p *Plugin) runCreate(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) { func (p *Plugin) runCreate(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) {
@ -105,7 +112,13 @@ func (p *Plugin) runCreate(args []string, extra *model.CommandArgs) (bool, *mode
restOfArgs := []string{} restOfArgs := []string{}
var handler func([]string, *model.CommandArgs) (bool, *model.CommandResponse, error) var handler func([]string, *model.CommandArgs) (bool, *model.CommandResponse, error)
if lengthOfArgs == 0 { if lengthOfArgs == 0 {
return false, &model.CommandResponse{Text: "Specify what you want to create."}, nil u, _ := p.mm.User.Get(extra.UserId)
locale := "ru"
if u != nil {
locale = u.Locale
}
T := p.getT(locale)
return false, &model.CommandResponse{Text: T("badges.error.specify_create", "Укажите, что вы хотите создать.")}, nil
} }
command := args[0] command := args[0]
if lengthOfArgs > 1 { if lengthOfArgs > 1 {
@ -117,7 +130,13 @@ func (p *Plugin) runCreate(args []string, extra *model.CommandArgs) (bool, *mode
case "type": case "type":
handler = p.runCreateType handler = p.runCreateType
default: default:
return false, &model.CommandResponse{Text: "You can create either badge or type"}, nil u, _ := p.mm.User.Get(extra.UserId)
locale := "ru"
if u != nil {
locale = u.Locale
}
T := p.getT(locale)
return false, &model.CommandResponse{Text: T("badges.error.create_badge_or_type", "Можно создать badge или type")}, nil
} }
return handler(restOfArgs, extra) return handler(restOfArgs, extra)
@ -128,6 +147,7 @@ func (p *Plugin) runCreateBadge(args []string, extra *model.CommandArgs) (bool,
if err != nil { if err != nil {
return commandError(err.Error()) return commandError(err.Error())
} }
T := p.getT(u.Locale)
typeSuggestions, err := p.filterCreateBadgeTypes(u) typeSuggestions, err := p.filterCreateBadgeTypes(u)
if err != nil { if err != nil {
@ -141,45 +161,45 @@ func (p *Plugin) runCreateBadge(args []string, extra *model.CommandArgs) (bool,
} }
if len(typeOptions) == 0 { if len(typeOptions) == 0 {
return commandError("You cannot create badges from any type.") return commandError(T("badges.error.no_types_available", "Вы не можете создать значки ни одного типа."))
} }
err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{ err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{
TriggerId: extra.TriggerId, TriggerId: extra.TriggerId,
URL: p.getDialogURL() + DialogPathCreateBadge, URL: p.getDialogURL() + DialogPathCreateBadge,
Dialog: model.Dialog{ Dialog: model.Dialog{
Title: "Create badge", Title: T("badges.dialog.create_badge.title", "Создать значок"),
SubmitLabel: "Create", SubmitLabel: T("badges.dialog.create_badge.submit", "Создать"),
Elements: []model.DialogElement{ Elements: []model.DialogElement{
{ {
DisplayName: "Name", DisplayName: T("badges.field.name", "Название"),
Type: "text", Type: "text",
Name: DialogFieldBadgeName, Name: DialogFieldBadgeName,
MaxLength: badgesmodel.NameMaxLength, MaxLength: badgesmodel.NameMaxLength,
}, },
{ {
DisplayName: "Description", DisplayName: T("badges.field.description", "Описание"),
Type: "text", Type: "text",
Name: DialogFieldBadgeDescription, Name: DialogFieldBadgeDescription,
MaxLength: badgesmodel.DescriptionMaxLength, MaxLength: badgesmodel.DescriptionMaxLength,
}, },
{ {
DisplayName: "Image", DisplayName: T("badges.field.image", "Изображение"),
Type: "text", Type: "text",
Name: DialogFieldBadgeImage, Name: DialogFieldBadgeImage,
HelpText: "Insert a emoticon name", HelpText: T("badges.field.image.help", "Введите название эмодзи"),
}, },
{ {
DisplayName: "Type", DisplayName: T("badges.field.type", "Тип"),
Type: "select", Type: "select",
Name: DialogFieldBadgeType, Name: DialogFieldBadgeType,
Options: typeOptions, Options: typeOptions,
}, },
{ {
DisplayName: "Multiple", DisplayName: T("badges.field.multiple", "Многократный"),
Type: "bool", Type: "bool",
Name: DialogFieldBadgeMultiple, Name: DialogFieldBadgeMultiple,
HelpText: "Whether the badge can be granted multiple times", HelpText: T("badges.field.multiple.help", "Можно ли выдавать этот значок несколько раз"),
Optional: true, Optional: true,
}, },
}, },
@ -198,7 +218,13 @@ func (p *Plugin) runEdit(args []string, extra *model.CommandArgs) (bool, *model.
restOfArgs := []string{} restOfArgs := []string{}
var handler func([]string, *model.CommandArgs) (bool, *model.CommandResponse, error) var handler func([]string, *model.CommandArgs) (bool, *model.CommandResponse, error)
if lengthOfArgs == 0 { if lengthOfArgs == 0 {
return false, &model.CommandResponse{Text: "Specify what you want to create."}, nil u, _ := p.mm.User.Get(extra.UserId)
locale := "ru"
if u != nil {
locale = u.Locale
}
T := p.getT(locale)
return false, &model.CommandResponse{Text: T("badges.error.specify_edit", "Укажите, что вы хотите отредактировать.")}, nil
} }
command := args[0] command := args[0]
if lengthOfArgs > 1 { if lengthOfArgs > 1 {
@ -210,7 +236,13 @@ func (p *Plugin) runEdit(args []string, extra *model.CommandArgs) (bool, *model.
case "type": case "type":
handler = p.runEditType handler = p.runEditType
default: default:
return false, &model.CommandResponse{Text: "You can create either badge or type"}, nil u, _ := p.mm.User.Get(extra.UserId)
locale := "ru"
if u != nil {
locale = u.Locale
}
T := p.getT(locale)
return false, &model.CommandResponse{Text: T("badges.error.edit_badge_or_type", "Можно редактировать badge или type")}, nil
} }
return handler(restOfArgs, extra) return handler(restOfArgs, extra)
@ -221,6 +253,7 @@ func (p *Plugin) runEditBadge(args []string, extra *model.CommandArgs) (bool, *m
if err != nil { if err != nil {
return commandError(err.Error()) return commandError(err.Error())
} }
T := p.getT(u.Locale)
var badgeIDStr string var badgeIDStr string
fs := pflag.NewFlagSet("", pflag.ContinueOnError) fs := pflag.NewFlagSet("", pflag.ContinueOnError)
@ -230,7 +263,7 @@ func (p *Plugin) runEditBadge(args []string, extra *model.CommandArgs) (bool, *m
} }
if badgeIDStr == "" { if badgeIDStr == "" {
return commandError("You must set the badge ID") return commandError(T("badges.error.must_set_badge_id", "Необходимо указать ID значка"))
} }
badge, err := p.store.GetBadge(badgesmodel.BadgeID(badgeIDStr)) badge, err := p.store.GetBadge(badgesmodel.BadgeID(badgeIDStr))
@ -239,7 +272,7 @@ func (p *Plugin) runEditBadge(args []string, extra *model.CommandArgs) (bool, *m
} }
if !canEditBadge(u, p.badgeAdminUserID, badge) { if !canEditBadge(u, p.badgeAdminUserID, badge) {
return commandError("you cannot edit this badge") return commandError(T("badges.error.cannot_edit_badge", "У вас нет прав на редактирование этого значка"))
} }
typeSuggestions, err := p.filterCreateBadgeTypes(u) typeSuggestions, err := p.filterCreateBadgeTypes(u)
@ -254,58 +287,58 @@ func (p *Plugin) runEditBadge(args []string, extra *model.CommandArgs) (bool, *m
} }
if len(typeOptions) == 0 { if len(typeOptions) == 0 {
return commandError("You cannot create badges from any type.") return commandError(T("badges.error.no_types_available", "Вы не можете создать значки ни одного типа."))
} }
err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{ err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{
TriggerId: extra.TriggerId, TriggerId: extra.TriggerId,
URL: p.getDialogURL() + DialogPathEditBadge, URL: p.getDialogURL() + DialogPathEditBadge,
Dialog: model.Dialog{ Dialog: model.Dialog{
Title: "Create badge", Title: T("badges.dialog.edit_badge.title", "Редактировать значок"),
SubmitLabel: "Edit", SubmitLabel: T("badges.dialog.edit_badge.submit", "Сохранить"),
State: string(badge.ID), State: string(badge.ID),
Elements: []model.DialogElement{ Elements: []model.DialogElement{
{ {
DisplayName: "Name", DisplayName: T("badges.field.name", "Название"),
Type: "text", Type: "text",
Name: DialogFieldBadgeName, Name: DialogFieldBadgeName,
MaxLength: badgesmodel.NameMaxLength, MaxLength: badgesmodel.NameMaxLength,
Default: badge.Name, Default: badge.Name,
}, },
{ {
DisplayName: "Description", DisplayName: T("badges.field.description", "Описание"),
Type: "text", Type: "text",
Name: DialogFieldBadgeDescription, Name: DialogFieldBadgeDescription,
MaxLength: badgesmodel.DescriptionMaxLength, MaxLength: badgesmodel.DescriptionMaxLength,
Default: badge.Description, Default: badge.Description,
}, },
{ {
DisplayName: "Image", DisplayName: T("badges.field.image", "Изображение"),
Type: "text", Type: "text",
Name: DialogFieldBadgeImage, Name: DialogFieldBadgeImage,
HelpText: "Insert a emoticon name", HelpText: T("badges.field.image.help", "Введите название эмодзи"),
Default: badge.Image, Default: badge.Image,
}, },
{ {
DisplayName: "Type", DisplayName: T("badges.field.type", "Тип"),
Type: "select", Type: "select",
Name: DialogFieldBadgeType, Name: DialogFieldBadgeType,
Options: typeOptions, Options: typeOptions,
Default: string(badge.Type), Default: string(badge.Type),
}, },
{ {
DisplayName: "Multiple", DisplayName: T("badges.field.multiple", "Многократный"),
Type: "bool", Type: "bool",
Name: DialogFieldBadgeMultiple, Name: DialogFieldBadgeMultiple,
HelpText: "Whether the badge can be granted multiple times", HelpText: T("badges.field.multiple.help", "Можно ли выдавать этот значок несколько раз"),
Optional: true, Optional: true,
Default: getBooleanString(badge.Multiple), Default: getBooleanString(badge.Multiple),
}, },
{ {
DisplayName: "Delete badge", DisplayName: T("badges.field.delete_badge", "Удалить значок"),
Type: "bool", Type: "bool",
Name: DialogFieldBadgeDelete, Name: DialogFieldBadgeDelete,
HelpText: "WARNING: Checking this will remove this badge permanently.", HelpText: T("badges.field.delete_badge.help", "ВНИМАНИЕ: если отметить, значок будет удалён безвозвратно."),
Optional: true, Optional: true,
}, },
}, },
@ -324,9 +357,10 @@ func (p *Plugin) runEditType(args []string, extra *model.CommandArgs) (bool, *mo
if err != nil { if err != nil {
return commandError(err.Error()) return commandError(err.Error())
} }
T := p.getT(u.Locale)
if !canCreateType(u, p.badgeAdminUserID, false) { if !canCreateType(u, p.badgeAdminUserID, false) {
return commandError("You have no permissions to edit a badge type.") return commandError(T("badges.error.no_permissions_edit_type", "У вас нет прав на редактирование типа значков."))
} }
var badgeTypeStr string var badgeTypeStr string
@ -337,7 +371,7 @@ func (p *Plugin) runEditType(args []string, extra *model.CommandArgs) (bool, *mo
} }
if badgeTypeStr == "" { if badgeTypeStr == "" {
return commandError("You must provide a type id") return commandError(T("badges.error.must_provide_type_id", "Необходимо указать ID типа"))
} }
typeDefinition, err := p.store.GetType(badgesmodel.BadgeType(badgeTypeStr)) typeDefinition, err := p.store.GetType(badgesmodel.BadgeType(badgeTypeStr))
@ -346,7 +380,7 @@ func (p *Plugin) runEditType(args []string, extra *model.CommandArgs) (bool, *mo
} }
if !canEditType(u, p.badgeAdminUserID, typeDefinition) { if !canEditType(u, p.badgeAdminUserID, typeDefinition) {
return commandError("you cannot edit this type") return commandError(T("badges.error.cannot_edit_type", "У вас нет прав на редактирование этого типа"))
} }
canGrantAllowList := "" canGrantAllowList := ""
@ -389,56 +423,56 @@ func (p *Plugin) runEditType(args []string, extra *model.CommandArgs) (bool, *mo
TriggerId: extra.TriggerId, TriggerId: extra.TriggerId,
URL: p.getDialogURL() + DialogPathEditType, URL: p.getDialogURL() + DialogPathEditType,
Dialog: model.Dialog{ Dialog: model.Dialog{
Title: "Edit type", Title: T("badges.dialog.edit_type.title", "Редактировать тип"),
SubmitLabel: "Edit", SubmitLabel: T("badges.dialog.edit_type.submit", "Сохранить"),
State: badgeTypeStr, State: badgeTypeStr,
Elements: []model.DialogElement{ Elements: []model.DialogElement{
{ {
DisplayName: "Name", DisplayName: T("badges.field.name", "Название"),
Type: "text", Type: "text",
Name: DialogFieldTypeName, Name: DialogFieldTypeName,
MaxLength: badgesmodel.NameMaxLength, MaxLength: badgesmodel.NameMaxLength,
Default: typeDefinition.Name, Default: typeDefinition.Name,
}, },
{ {
DisplayName: "Everyone can create badge", DisplayName: T("badges.field.everyone_can_create", "Все могут создавать значки"),
Type: "bool", Type: "bool",
Name: DialogFieldTypeEveryoneCanCreate, Name: DialogFieldTypeEveryoneCanCreate,
HelpText: "Whether any user can create a badge of this type", HelpText: T("badges.field.everyone_can_create.help", "Любой пользователь может создать значок этого типа"),
Optional: true, Optional: true,
Default: getBooleanString(typeDefinition.CanCreate.Everyone), Default: getBooleanString(typeDefinition.CanCreate.Everyone),
}, },
{ {
DisplayName: "Can create allowlist", DisplayName: T("badges.field.allowlist_create", "Список допущенных к созданию"),
Type: "text", Type: "text",
Name: DialogFieldTypeAllowlistCanCreate, Name: DialogFieldTypeAllowlistCanCreate,
HelpText: "Fill the usernames separated by comma (,) of the people that can create badges of this type.", HelpText: T("badges.field.allowlist_create.help", "Укажите имена пользователей через запятую (,), которые могут создавать значки этого типа."),
Placeholder: "user-1, user-2, user-3", Placeholder: "user-1, user-2, user-3",
Optional: true, Optional: true,
Default: canCreateAllowList, Default: canCreateAllowList,
}, },
{ {
DisplayName: "Everyone can grant badge", DisplayName: T("badges.field.everyone_can_grant", "Все могут выдавать значки"),
Type: "bool", Type: "bool",
Name: DialogFieldTypeEveryoneCanGrant, Name: DialogFieldTypeEveryoneCanGrant,
HelpText: "Whether any user can grant a badge of this type", HelpText: T("badges.field.everyone_can_grant.help", "Любой пользователь может выдать значок этого типа"),
Optional: true, Optional: true,
Default: getBooleanString(typeDefinition.CanGrant.Everyone), Default: getBooleanString(typeDefinition.CanGrant.Everyone),
}, },
{ {
DisplayName: "Can grant allowlist", DisplayName: T("badges.field.allowlist_grant", "Список допущенных к выдаче"),
Type: "text", Type: "text",
Name: DialogFieldTypeAllowlistCanGrant, Name: DialogFieldTypeAllowlistCanGrant,
HelpText: "Fill the usernames separated by comma (,) of the people that can grant badges of this type.", HelpText: T("badges.field.allowlist_grant.help", "Укажите имена пользователей через запятую (,), которые могут выдавать значки этого типа."),
Placeholder: "user-1, user-2, user-3", Placeholder: "user-1, user-2, user-3",
Optional: true, Optional: true,
Default: canGrantAllowList, Default: canGrantAllowList,
}, },
{ {
DisplayName: "Remove type", DisplayName: T("badges.field.delete_type", "Удалить тип"),
Type: "bool", Type: "bool",
Name: DialogFieldTypeDelete, Name: DialogFieldTypeDelete,
HelpText: "WARNING: checking this will remove this type and all associated badges permanently.", HelpText: T("badges.field.delete_type.help", "ВНИМАНИЕ: если отметить, этот тип и все связанные значки будут удалены безвозвратно."),
Optional: true, Optional: true,
}, },
}, },
@ -457,51 +491,52 @@ func (p *Plugin) runCreateType(args []string, extra *model.CommandArgs) (bool, *
if err != nil { if err != nil {
return commandError(err.Error()) return commandError(err.Error())
} }
T := p.getT(u.Locale)
if !canCreateType(u, p.badgeAdminUserID, false) { if !canCreateType(u, p.badgeAdminUserID, false) {
return commandError("You have no permissions to create a badge type.") return commandError(T("badges.error.no_permissions_create_type", "У вас нет прав на создание типа значков."))
} }
err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{ err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{
TriggerId: extra.TriggerId, TriggerId: extra.TriggerId,
URL: p.getDialogURL() + DialogPathCreateType, URL: p.getDialogURL() + DialogPathCreateType,
Dialog: model.Dialog{ Dialog: model.Dialog{
Title: "Create type", Title: T("badges.dialog.create_type.title", "Создать тип"),
SubmitLabel: "Create", SubmitLabel: T("badges.dialog.create_type.submit", "Создать"),
Elements: []model.DialogElement{ Elements: []model.DialogElement{
{ {
DisplayName: "Name", DisplayName: T("badges.field.name", "Название"),
Type: "text", Type: "text",
Name: DialogFieldTypeName, Name: DialogFieldTypeName,
MaxLength: badgesmodel.NameMaxLength, MaxLength: badgesmodel.NameMaxLength,
}, },
{ {
DisplayName: "Everyone can create badge", DisplayName: T("badges.field.everyone_can_create", "Все могут создавать значки"),
Type: "bool", Type: "bool",
Name: DialogFieldTypeEveryoneCanCreate, Name: DialogFieldTypeEveryoneCanCreate,
HelpText: "Whether any user can create a badge of this type", HelpText: T("badges.field.everyone_can_create.help", "Любой пользователь может создать значок этого типа"),
Optional: true, Optional: true,
}, },
{ {
DisplayName: "Can create allowlist", DisplayName: T("badges.field.allowlist_create", "Список допущенных к созданию"),
Type: "text", Type: "text",
Name: DialogFieldTypeAllowlistCanCreate, Name: DialogFieldTypeAllowlistCanCreate,
HelpText: "Fill the usernames separated by comma (,) of the people that can create badges of this type.", HelpText: T("badges.field.allowlist_create.help", "Укажите имена пользователей через запятую (,), которые могут создавать значки этого типа."),
Placeholder: "user-1, user-2, user-3", Placeholder: "user-1, user-2, user-3",
Optional: true, Optional: true,
}, },
{ {
DisplayName: "Everyone can grant badge", DisplayName: T("badges.field.everyone_can_grant", "Все могут выдавать значки"),
Type: "bool", Type: "bool",
Name: DialogFieldTypeEveryoneCanGrant, Name: DialogFieldTypeEveryoneCanGrant,
HelpText: "Whether any user can grant a badge of this type", HelpText: T("badges.field.everyone_can_grant.help", "Любой пользователь может выдать значок этого типа"),
Optional: true, Optional: true,
}, },
{ {
DisplayName: "Can grant allowlist", DisplayName: T("badges.field.allowlist_grant", "Список допущенных к выдаче"),
Type: "text", Type: "text",
Name: DialogFieldTypeAllowlistCanGrant, Name: DialogFieldTypeAllowlistCanGrant,
HelpText: "Fill the usernames separated by comma (,) of the people that can grant badges of this type.", HelpText: T("badges.field.allowlist_grant.help", "Укажите имена пользователей через запятую (,), которые могут выдавать значки этого типа."),
Placeholder: "user-1, user-2, user-3", Placeholder: "user-1, user-2, user-3",
Optional: true, Optional: true,
}, },
@ -535,6 +570,7 @@ func (p *Plugin) runGrant(args []string, extra *model.CommandArgs) (bool, *model
if err != nil { if err != nil {
return commandError(err.Error()) return commandError(err.Error())
} }
T := p.getT(granter.Locale)
badge, err := p.store.GetBadge(badgesmodel.BadgeID(badgeStr)) badge, err := p.store.GetBadge(badgesmodel.BadgeID(badgeStr))
if err != nil { if err != nil {
@ -547,7 +583,7 @@ func (p *Plugin) runGrant(args []string, extra *model.CommandArgs) (bool, *model
} }
if !canGrantBadge(granter, p.badgeAdminUserID, badge, badgeType) { if !canGrantBadge(granter, p.badgeAdminUserID, badge, badgeType) {
return commandError("you have no permissions to grant this badge") return commandError(T("badges.error.no_permissions_grant", "У вас нет прав на выдачу этого значка"))
} }
user, err := p.mm.User.GetByUsername(username) user, err := p.mm.User.GetByUsername(username)
@ -564,10 +600,16 @@ func (p *Plugin) runGrant(args []string, extra *model.CommandArgs) (bool, *model
p.notifyGrant(badgesmodel.BadgeID(badgeStr), extra.UserId, user, false, "", "") p.notifyGrant(badgesmodel.BadgeID(badgeStr), extra.UserId, user, false, "", "")
} }
p.postCommandResponse(extra, "Granted") p.postCommandResponse(extra, T("badges.success.granted", "Выдано"))
return false, &model.CommandResponse{}, nil return false, &model.CommandResponse{}, nil
} }
actingUser, err := p.mm.User.Get(extra.UserId)
if err != nil {
return commandError(err.Error())
}
T := p.getT(actingUser.Locale)
elements := []model.DialogElement{} elements := []model.DialogElement{}
stateText := "" stateText := ""
@ -582,24 +624,19 @@ func (p *Plugin) runGrant(args []string, extra *model.CommandArgs) (bool, *model
return commandError(err.Error()) return commandError(err.Error())
} }
introductionText = "Grant badge to @" + username introductionText = T("badges.dialog.grant.intro", "Выдать значок пользователю @%s", username)
stateText = user.Id stateText = user.Id
} }
if stateText == "" { if stateText == "" {
elements = append(elements, model.DialogElement{ elements = append(elements, model.DialogElement{
DisplayName: "User", DisplayName: T("badges.field.user", "Пользователь"),
Type: "select", Type: "select",
Name: DialogFieldUser, Name: DialogFieldUser,
DataSource: "users", DataSource: "users",
}) })
} }
actingUser, err := p.mm.User.Get(extra.UserId)
if err != nil {
return commandError(err.Error())
}
options := []*model.PostActionOptions{} options := []*model.PostActionOptions{}
grantableBadges, err := p.filterGrantBadges(actingUser) grantableBadges, err := p.filterGrantBadges(actingUser)
if err != nil { if err != nil {
@ -610,7 +647,7 @@ func (p *Plugin) runGrant(args []string, extra *model.CommandArgs) (bool, *model
} }
badgeElement := model.DialogElement{ badgeElement := model.DialogElement{
DisplayName: "Badge", DisplayName: T("badges.field.badge", "Значок"),
Type: "select", Type: "select",
Name: DialogFieldBadge, Name: DialogFieldBadge,
Options: options, Options: options,
@ -626,7 +663,7 @@ func (p *Plugin) runGrant(args []string, extra *model.CommandArgs) (bool, *model
} }
if !found { if !found {
return commandError("You cannot grant that badge") return commandError(T("badges.error.cannot_grant_badge", "Вы не можете выдать этот значок"))
} }
badgeElement.Default = badgeStr badgeElement.Default = badgeStr
@ -635,18 +672,18 @@ func (p *Plugin) runGrant(args []string, extra *model.CommandArgs) (bool, *model
elements = append(elements, badgeElement) elements = append(elements, badgeElement)
elements = append(elements, model.DialogElement{ elements = append(elements, model.DialogElement{
DisplayName: "Reason", DisplayName: T("badges.field.reason", "Причина"),
Name: DialogFieldGrantReason, Name: DialogFieldGrantReason,
Optional: true, Optional: true,
HelpText: "Reason why you are granting this badge. This will be seen by the user, and wherever this grant notification is shown (e.g. subscriptions).", HelpText: T("badges.field.reason.help", "Причина выдачи значка. Будет видна пользователю и в уведомлениях о выдаче (например, в подписках)."),
Type: "text", Type: "text",
}) })
elements = append(elements, model.DialogElement{ elements = append(elements, model.DialogElement{
DisplayName: "Notify on this channel", DisplayName: T("badges.field.notify_here", "Уведомить в этом канале"),
Name: DialogFieldNotifyHere, Name: DialogFieldNotifyHere,
Type: "bool", Type: "bool",
HelpText: "If you mark this, the bot will send a message to this channel notifying that you granted this badge to this person.", HelpText: T("badges.field.notify_here.help", "Если отметить, бот отправит сообщение в этот канал о том, что вы выдали значок этому пользователю."),
Optional: true, Optional: true,
}) })
@ -654,9 +691,9 @@ func (p *Plugin) runGrant(args []string, extra *model.CommandArgs) (bool, *model
TriggerId: extra.TriggerId, TriggerId: extra.TriggerId,
URL: p.getDialogURL() + DialogPathGrant, URL: p.getDialogURL() + DialogPathGrant,
Dialog: model.Dialog{ Dialog: model.Dialog{
Title: "Grant badge", Title: T("badges.dialog.grant.title", "Выдать значок"),
IntroductionText: introductionText, IntroductionText: introductionText,
SubmitLabel: "Grant", SubmitLabel: T("badges.dialog.grant.submit", "Выдать"),
Elements: elements, Elements: elements,
State: stateText, State: stateText,
}, },
@ -674,7 +711,13 @@ func (p *Plugin) runSubscription(args []string, extra *model.CommandArgs) (bool,
restOfArgs := []string{} restOfArgs := []string{}
var handler func([]string, *model.CommandArgs) (bool, *model.CommandResponse, error) var handler func([]string, *model.CommandArgs) (bool, *model.CommandResponse, error)
if lengthOfArgs == 0 { if lengthOfArgs == 0 {
return false, &model.CommandResponse{Text: "Specify what you want to do."}, nil u, _ := p.mm.User.Get(extra.UserId)
locale := "ru"
if u != nil {
locale = u.Locale
}
T := p.getT(locale)
return false, &model.CommandResponse{Text: T("badges.error.specify_subscription", "Укажите, что вы хотите сделать.")}, nil
} }
command := args[0] command := args[0]
if lengthOfArgs > 1 { if lengthOfArgs > 1 {
@ -686,7 +729,13 @@ func (p *Plugin) runSubscription(args []string, extra *model.CommandArgs) (bool,
case "remove": case "remove":
handler = p.runDeleteSubscription handler = p.runDeleteSubscription
default: default:
return false, &model.CommandResponse{Text: "You can either create or delete subscriptions"}, nil u, _ := p.mm.User.Get(extra.UserId)
locale := "ru"
if u != nil {
locale = u.Locale
}
T := p.getT(locale)
return false, &model.CommandResponse{Text: T("badges.error.create_or_delete_subscription", "Можно создать или удалить подписку")}, nil
} }
return handler(restOfArgs, extra) return handler(restOfArgs, extra)
@ -704,9 +753,10 @@ func (p *Plugin) runCreateSubscription(args []string, extra *model.CommandArgs)
if err != nil { if err != nil {
return commandError(err.Error()) return commandError(err.Error())
} }
T := p.getT(actingUser.Locale)
if !canCreateSubscription(actingUser, p.badgeAdminUserID, extra.ChannelId) { if !canCreateSubscription(actingUser, p.badgeAdminUserID, extra.ChannelId) {
return commandError("You cannot create subscriptions") return commandError(T("badges.error.cannot_create_subscription", "Вы не можете создавать подписки"))
} }
if typeStr != "" { if typeStr != "" {
@ -716,7 +766,7 @@ func (p *Plugin) runCreateSubscription(args []string, extra *model.CommandArgs)
return commandError(err.Error()) return commandError(err.Error())
} }
p.postCommandResponse(extra, "Granted") p.postCommandResponse(extra, T("badges.success.granted", "Выдано"))
return false, &model.CommandResponse{}, nil return false, &model.CommandResponse{}, nil
} }
@ -733,12 +783,12 @@ func (p *Plugin) runCreateSubscription(args []string, extra *model.CommandArgs)
TriggerId: extra.TriggerId, TriggerId: extra.TriggerId,
URL: p.getDialogURL() + DialogPathCreateSubscription, URL: p.getDialogURL() + DialogPathCreateSubscription,
Dialog: model.Dialog{ Dialog: model.Dialog{
Title: "Create subscription", Title: T("badges.dialog.create_subscription.title", "Создать подписку"),
IntroductionText: "Introduce the badge type you want to subscribe to this channel.", IntroductionText: T("badges.dialog.create_subscription.intro", "Выберите тип значка, на который хотите подписать этот канал."),
SubmitLabel: "Add", SubmitLabel: T("badges.dialog.create_subscription.submit", "Добавить"),
Elements: []model.DialogElement{ Elements: []model.DialogElement{
{ {
DisplayName: "Type", DisplayName: T("badges.field.type", "Тип"),
Type: "select", Type: "select",
Name: DialogFieldBadgeType, Name: DialogFieldBadgeType,
Options: options, Options: options,
@ -766,9 +816,10 @@ func (p *Plugin) runDeleteSubscription(args []string, extra *model.CommandArgs)
if err != nil { if err != nil {
return commandError(err.Error()) return commandError(err.Error())
} }
T := p.getT(actingUser.Locale)
if !canCreateSubscription(actingUser, p.badgeAdminUserID, extra.ChannelId) { if !canCreateSubscription(actingUser, p.badgeAdminUserID, extra.ChannelId) {
return commandError("You cannot create subscriptions") return commandError(T("badges.error.cannot_create_subscription", "Вы не можете создавать подписки"))
} }
if typeStr != "" { if typeStr != "" {
@ -777,7 +828,7 @@ func (p *Plugin) runDeleteSubscription(args []string, extra *model.CommandArgs)
return commandError(err.Error()) return commandError(err.Error())
} }
p.postCommandResponse(extra, "Removed") p.postCommandResponse(extra, T("badges.success.removed", "Удалено"))
return false, &model.CommandResponse{}, nil return false, &model.CommandResponse{}, nil
} }
@ -794,12 +845,12 @@ func (p *Plugin) runDeleteSubscription(args []string, extra *model.CommandArgs)
TriggerId: extra.TriggerId, TriggerId: extra.TriggerId,
URL: p.getDialogURL() + DialogPathDeleteSubscription, URL: p.getDialogURL() + DialogPathDeleteSubscription,
Dialog: model.Dialog{ Dialog: model.Dialog{
Title: "Delete subscription", Title: T("badges.dialog.delete_subscription.title", "Удалить подписку"),
IntroductionText: "Introduce the badge type you want to remove from this channel.", IntroductionText: T("badges.dialog.delete_subscription.intro", "Выберите тип значка, подписку на который хотите удалить из этого канала."),
SubmitLabel: "Remove", SubmitLabel: T("badges.dialog.delete_subscription.submit", "Удалить"),
Elements: []model.DialogElement{ Elements: []model.DialogElement{
{ {
DisplayName: "Type", DisplayName: T("badges.field.type", "Тип"),
Type: "select", Type: "select",
Name: DialogFieldBadgeType, Name: DialogFieldBadgeType,
Options: options, Options: options,

106
server/i18n/en.json Normal file
View File

@ -0,0 +1,106 @@
[
{"id": "badges.dialog.create_badge.title", "translation": "Create badge"},
{"id": "badges.dialog.create_badge.submit", "translation": "Create"},
{"id": "badges.dialog.edit_badge.title", "translation": "Edit badge"},
{"id": "badges.dialog.edit_badge.submit", "translation": "Save"},
{"id": "badges.dialog.create_type.title", "translation": "Create type"},
{"id": "badges.dialog.create_type.submit", "translation": "Create"},
{"id": "badges.dialog.edit_type.title", "translation": "Edit type"},
{"id": "badges.dialog.edit_type.submit", "translation": "Save"},
{"id": "badges.dialog.grant.title", "translation": "Grant badge"},
{"id": "badges.dialog.grant.submit", "translation": "Grant"},
{"id": "badges.dialog.grant.intro", "translation": "Grant badge to @%s"},
{"id": "badges.dialog.create_subscription.title", "translation": "Create subscription"},
{"id": "badges.dialog.create_subscription.submit", "translation": "Add"},
{"id": "badges.dialog.create_subscription.intro", "translation": "Select the badge type you want to subscribe to this channel."},
{"id": "badges.dialog.delete_subscription.title", "translation": "Delete subscription"},
{"id": "badges.dialog.delete_subscription.submit", "translation": "Remove"},
{"id": "badges.dialog.delete_subscription.intro", "translation": "Select the badge type you want to unsubscribe from this channel."},
{"id": "badges.field.name", "translation": "Name"},
{"id": "badges.field.description", "translation": "Description"},
{"id": "badges.field.image", "translation": "Image"},
{"id": "badges.field.image.help", "translation": "Enter an emoticon name"},
{"id": "badges.field.type", "translation": "Type"},
{"id": "badges.field.multiple", "translation": "Multiple"},
{"id": "badges.field.multiple.help", "translation": "Whether the badge can be granted multiple times"},
{"id": "badges.field.delete_badge", "translation": "Delete badge"},
{"id": "badges.field.delete_badge.help", "translation": "WARNING: checking this will remove this badge permanently."},
{"id": "badges.field.everyone_can_create", "translation": "Everyone can create badge"},
{"id": "badges.field.everyone_can_create.help", "translation": "Whether any user can create a badge of this type"},
{"id": "badges.field.allowlist_create", "translation": "Can create allowlist"},
{"id": "badges.field.allowlist_create.help", "translation": "Fill the usernames separated by comma (,) of the people that can create badges of this type."},
{"id": "badges.field.everyone_can_grant", "translation": "Everyone can grant badge"},
{"id": "badges.field.everyone_can_grant.help", "translation": "Whether any user can grant a badge of this type"},
{"id": "badges.field.allowlist_grant", "translation": "Can grant allowlist"},
{"id": "badges.field.allowlist_grant.help", "translation": "Fill the usernames separated by comma (,) of the people that can grant badges of this type."},
{"id": "badges.field.delete_type", "translation": "Remove type"},
{"id": "badges.field.delete_type.help", "translation": "WARNING: checking this will remove this type and all associated badges permanently."},
{"id": "badges.field.user", "translation": "User"},
{"id": "badges.field.badge", "translation": "Badge"},
{"id": "badges.field.reason", "translation": "Reason"},
{"id": "badges.field.reason.help", "translation": "Reason why you are granting this badge. This will be seen by the user, and wherever this grant notification is shown (e.g. subscriptions)."},
{"id": "badges.field.notify_here", "translation": "Notify on this channel"},
{"id": "badges.field.notify_here.help", "translation": "If you mark this, the bot will send a message to this channel notifying that you granted this badge to this person."},
{"id": "badges.error.unknown", "translation": "An unknown error occurred. Please talk to your system administrator for help."},
{"id": "badges.error.cannot_get_user", "translation": "Cannot get user."},
{"id": "badges.error.only_sysadmin_clean", "translation": "Only a system admin can clean the badges database."},
{"id": "badges.error.specify_create", "translation": "Specify what you want to create."},
{"id": "badges.error.create_badge_or_type", "translation": "You can create either badge or type"},
{"id": "badges.error.no_types_available", "translation": "You cannot create badges from any type."},
{"id": "badges.error.must_set_badge_id", "translation": "You must set the badge ID"},
{"id": "badges.error.cannot_edit_badge", "translation": "You cannot edit this badge"},
{"id": "badges.error.specify_edit", "translation": "Specify what you want to edit."},
{"id": "badges.error.edit_badge_or_type", "translation": "You can edit either badge or type"},
{"id": "badges.error.no_permissions_edit_type", "translation": "You have no permissions to edit a badge type."},
{"id": "badges.error.must_provide_type_id", "translation": "You must provide a type id"},
{"id": "badges.error.cannot_edit_type", "translation": "You cannot edit this type"},
{"id": "badges.error.no_permissions_grant", "translation": "You have no permissions to grant this badge"},
{"id": "badges.error.cannot_grant_badge", "translation": "You cannot grant that badge"},
{"id": "badges.error.specify_subscription", "translation": "Specify what you want to do."},
{"id": "badges.error.create_or_delete_subscription", "translation": "You can either create or delete subscriptions"},
{"id": "badges.error.cannot_create_subscription", "translation": "You cannot create subscriptions"},
{"id": "badges.error.no_permissions_create_type", "translation": "You have no permissions to create a badge type."},
{"id": "badges.success.clean", "translation": "Clean"},
{"id": "badges.success.granted", "translation": "Granted"},
{"id": "badges.success.removed", "translation": "Removed"},
{"id": "badges.api.dialog_parse_error", "translation": "Could not get the dialog request"},
{"id": "badges.api.cannot_get_user", "translation": "Cannot get user"},
{"id": "badges.api.empty_emoji", "translation": "Empty emoji"},
{"id": "badges.api.invalid_field", "translation": "Invalid field"},
{"id": "badges.api.type_not_exist", "translation": "This type does not exist"},
{"id": "badges.api.no_permissions_create_badge", "translation": "You have no permissions to create this badge"},
{"id": "badges.api.badge_created", "translation": "Badge `%s` created."},
{"id": "badges.api.no_permissions_create_type", "translation": "You have no permissions to create a type"},
{"id": "badges.api.cannot_find_user", "translation": "Cannot find user"},
{"id": "badges.api.error_getting_user", "translation": "Error getting user %s: %v"},
{"id": "badges.api.type_created", "translation": "Type `%s` created."},
{"id": "badges.api.cannot_get_type", "translation": "Cannot get type"},
{"id": "badges.api.cannot_edit_type", "translation": "You cannot edit this type"},
{"id": "badges.api.could_not_get_type", "translation": "Could not get the type"},
{"id": "badges.api.no_permissions_edit_type", "translation": "You have no permissions to edit this type"},
{"id": "badges.api.type_updated", "translation": "Type `%s` updated."},
{"id": "badges.api.cannot_get_badge", "translation": "Cannot get badge"},
{"id": "badges.api.cannot_edit_badge", "translation": "You cannot edit this badge"},
{"id": "badges.api.could_not_get_badge", "translation": "Could not get the badge"},
{"id": "badges.api.no_permissions_edit_badge", "translation": "You have no permissions to edit this badge"},
{"id": "badges.api.badge_updated", "translation": "Badge `%s` updated."},
{"id": "badges.api.badge_not_found", "translation": "Badge not found"},
{"id": "badges.api.no_permissions_grant", "translation": "You have no permissions to grant this badge"},
{"id": "badges.api.user_not_found", "translation": "User not found"},
{"id": "badges.api.badge_granted", "translation": "Badge `%s` granted to @%s."},
{"id": "badges.api.cannot_create_subscription", "translation": "You cannot create a subscription"},
{"id": "badges.api.subscription_added", "translation": "Subscription added"},
{"id": "badges.api.cannot_delete_subscription", "translation": "You cannot delete a subscription"},
{"id": "badges.api.subscription_removed", "translation": "Subscription removed"},
{"id": "badges.api.not_authorized", "translation": "Not authorized"},
{"id": "badges.notify.dm_text", "translation": "@%s granted you the %s`%s` badge."},
{"id": "badges.notify.dm_reason", "translation": "\nWhy? "},
{"id": "badges.notify.title", "translation": "%sbadge granted!"},
{"id": "badges.notify.channel_text", "translation": "@%s granted @%s the %s`%s` badge."},
{"id": "badges.notify.no_permission_channel", "translation": "You don't have permissions to notify the grant on this channel."}
]

44
server/i18n/i18n.go Normal file
View File

@ -0,0 +1,44 @@
package i18n
import (
"embed"
"fmt"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
//go:embed *.json
var i18nFiles embed.FS
type TranslationFunc func(translationId string, defaultMessage string, params ...any) string
type Bundle i18n.Bundle
func Init() *Bundle {
bundle := i18n.NewBundle(language.Russian)
_, _ = bundle.LoadMessageFileFS(i18nFiles, "en.json")
return (*Bundle)(bundle)
}
func LocalizerFunc(bundle *Bundle, lang string) TranslationFunc {
localizer := i18n.NewLocalizer((*i18n.Bundle)(bundle), lang)
return func(translationId string, defaultMessage string, params ...any) string {
if len(params) > 0 {
return fmt.Sprintf(localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: translationId,
Other: defaultMessage,
},
}), params...)
}
return localizer.MustLocalize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: translationId,
Other: defaultMessage,
},
})
}
}

106
server/i18n/ru.json Normal file
View File

@ -0,0 +1,106 @@
[
{"id": "badges.dialog.create_badge.title", "translation": "Создать значок"},
{"id": "badges.dialog.create_badge.submit", "translation": "Создать"},
{"id": "badges.dialog.edit_badge.title", "translation": "Редактировать значок"},
{"id": "badges.dialog.edit_badge.submit", "translation": "Сохранить"},
{"id": "badges.dialog.create_type.title", "translation": "Создать тип"},
{"id": "badges.dialog.create_type.submit", "translation": "Создать"},
{"id": "badges.dialog.edit_type.title", "translation": "Редактировать тип"},
{"id": "badges.dialog.edit_type.submit", "translation": "Сохранить"},
{"id": "badges.dialog.grant.title", "translation": "Выдать значок"},
{"id": "badges.dialog.grant.submit", "translation": "Выдать"},
{"id": "badges.dialog.grant.intro", "translation": "Выдать значок пользователю @%s"},
{"id": "badges.dialog.create_subscription.title", "translation": "Создать подписку"},
{"id": "badges.dialog.create_subscription.submit", "translation": "Добавить"},
{"id": "badges.dialog.create_subscription.intro", "translation": "Выберите тип значка, на который хотите подписать этот канал."},
{"id": "badges.dialog.delete_subscription.title", "translation": "Удалить подписку"},
{"id": "badges.dialog.delete_subscription.submit", "translation": "Удалить"},
{"id": "badges.dialog.delete_subscription.intro", "translation": "Выберите тип значка, подписку на который хотите удалить из этого канала."},
{"id": "badges.field.name", "translation": "Название"},
{"id": "badges.field.description", "translation": "Описание"},
{"id": "badges.field.image", "translation": "Изображение"},
{"id": "badges.field.image.help", "translation": "Введите название эмодзи"},
{"id": "badges.field.type", "translation": "Тип"},
{"id": "badges.field.multiple", "translation": "Многократный"},
{"id": "badges.field.multiple.help", "translation": "Можно ли выдавать этот значок несколько раз"},
{"id": "badges.field.delete_badge", "translation": "Удалить значок"},
{"id": "badges.field.delete_badge.help", "translation": "ВНИМАНИЕ: если отметить, значок будет удалён безвозвратно."},
{"id": "badges.field.everyone_can_create", "translation": "Все могут создавать значки"},
{"id": "badges.field.everyone_can_create.help", "translation": "Любой пользователь может создать значок этого типа"},
{"id": "badges.field.allowlist_create", "translation": "Список допущенных к созданию"},
{"id": "badges.field.allowlist_create.help", "translation": "Укажите имена пользователей через запятую (,), которые могут создавать значки этого типа."},
{"id": "badges.field.everyone_can_grant", "translation": "Все могут выдавать значки"},
{"id": "badges.field.everyone_can_grant.help", "translation": "Любой пользователь может выдать значок этого типа"},
{"id": "badges.field.allowlist_grant", "translation": "Список допущенных к выдаче"},
{"id": "badges.field.allowlist_grant.help", "translation": "Укажите имена пользователей через запятую (,), которые могут выдавать значки этого типа."},
{"id": "badges.field.delete_type", "translation": "Удалить тип"},
{"id": "badges.field.delete_type.help", "translation": "ВНИМАНИЕ: если отметить, этот тип и все связанные значки будут удалены безвозвратно."},
{"id": "badges.field.user", "translation": "Пользователь"},
{"id": "badges.field.badge", "translation": "Значок"},
{"id": "badges.field.reason", "translation": "Причина"},
{"id": "badges.field.reason.help", "translation": "Причина выдачи значка. Будет видна пользователю и в уведомлениях о выдаче (например, в подписках)."},
{"id": "badges.field.notify_here", "translation": "Уведомить в этом канале"},
{"id": "badges.field.notify_here.help", "translation": "Если отметить, бот отправит сообщение в этот канал о том, что вы выдали значок этому пользователю."},
{"id": "badges.error.unknown", "translation": "Произошла неизвестная ошибка. Обратитесь к системному администратору."},
{"id": "badges.error.cannot_get_user", "translation": "Не удалось получить пользователя."},
{"id": "badges.error.only_sysadmin_clean", "translation": "Только системный администратор может очистить базу значков."},
{"id": "badges.error.specify_create", "translation": "Укажите, что вы хотите создать."},
{"id": "badges.error.create_badge_or_type", "translation": "Можно создать badge или type"},
{"id": "badges.error.no_types_available", "translation": "Вы не можете создать значки ни одного типа."},
{"id": "badges.error.must_set_badge_id", "translation": "Необходимо указать ID значка"},
{"id": "badges.error.cannot_edit_badge", "translation": "У вас нет прав на редактирование этого значка"},
{"id": "badges.error.specify_edit", "translation": "Укажите, что вы хотите отредактировать."},
{"id": "badges.error.edit_badge_or_type", "translation": "Можно редактировать badge или type"},
{"id": "badges.error.no_permissions_edit_type", "translation": "У вас нет прав на редактирование типа значков."},
{"id": "badges.error.must_provide_type_id", "translation": "Необходимо указать ID типа"},
{"id": "badges.error.cannot_edit_type", "translation": "У вас нет прав на редактирование этого типа"},
{"id": "badges.error.no_permissions_grant", "translation": "У вас нет прав на выдачу этого значка"},
{"id": "badges.error.cannot_grant_badge", "translation": "Вы не можете выдать этот значок"},
{"id": "badges.error.specify_subscription", "translation": "Укажите, что вы хотите сделать."},
{"id": "badges.error.create_or_delete_subscription", "translation": "Можно создать или удалить подписку"},
{"id": "badges.error.cannot_create_subscription", "translation": "Вы не можете создавать подписки"},
{"id": "badges.error.no_permissions_create_type", "translation": "У вас нет прав на создание типа значков."},
{"id": "badges.success.clean", "translation": "Очищено"},
{"id": "badges.success.granted", "translation": "Выдано"},
{"id": "badges.success.removed", "translation": "Удалено"},
{"id": "badges.api.dialog_parse_error", "translation": "Не удалось получить данные диалога"},
{"id": "badges.api.cannot_get_user", "translation": "Не удалось найти пользователя"},
{"id": "badges.api.empty_emoji", "translation": "Пустой эмодзи"},
{"id": "badges.api.invalid_field", "translation": "Некорректное поле"},
{"id": "badges.api.type_not_exist", "translation": "Этот тип не существует"},
{"id": "badges.api.no_permissions_create_badge", "translation": "У вас нет прав на создание этого значка"},
{"id": "badges.api.badge_created", "translation": "Значок `%s` создан."},
{"id": "badges.api.no_permissions_create_type", "translation": "У вас нет прав на создание типа"},
{"id": "badges.api.cannot_find_user", "translation": "Не удалось найти пользователя"},
{"id": "badges.api.error_getting_user", "translation": "Ошибка получения пользователя %s: %v"},
{"id": "badges.api.type_created", "translation": "Тип `%s` создан."},
{"id": "badges.api.cannot_get_type", "translation": "Не удалось получить тип"},
{"id": "badges.api.cannot_edit_type", "translation": "Вы не можете редактировать этот тип"},
{"id": "badges.api.could_not_get_type", "translation": "Не удалось получить тип"},
{"id": "badges.api.no_permissions_edit_type", "translation": "У вас нет прав на редактирование этого типа"},
{"id": "badges.api.type_updated", "translation": "Тип `%s` обновлён."},
{"id": "badges.api.cannot_get_badge", "translation": "Не удалось получить значок"},
{"id": "badges.api.cannot_edit_badge", "translation": "Вы не можете редактировать этот значок"},
{"id": "badges.api.could_not_get_badge", "translation": "Не удалось получить значок"},
{"id": "badges.api.no_permissions_edit_badge", "translation": "У вас нет прав на редактирование этого значка"},
{"id": "badges.api.badge_updated", "translation": "Значок `%s` обновлён."},
{"id": "badges.api.badge_not_found", "translation": "Значок не найден"},
{"id": "badges.api.no_permissions_grant", "translation": "У вас нет прав на выдачу этого значка"},
{"id": "badges.api.user_not_found", "translation": "Пользователь не найден"},
{"id": "badges.api.badge_granted", "translation": "Значок `%s` выдан @%s."},
{"id": "badges.api.cannot_create_subscription", "translation": "Вы не можете создать подписку"},
{"id": "badges.api.subscription_added", "translation": "Подписка добавлена"},
{"id": "badges.api.cannot_delete_subscription", "translation": "Вы не можете удалить подписку"},
{"id": "badges.api.subscription_removed", "translation": "Подписка удалена"},
{"id": "badges.api.not_authorized", "translation": "Не авторизован"},
{"id": "badges.notify.dm_text", "translation": "@%s выдал вам значок %s`%s`."},
{"id": "badges.notify.dm_reason", "translation": "\nПочему? "},
{"id": "badges.notify.title", "translation": "%sзначок выдан!"},
{"id": "badges.notify.channel_text", "translation": "@%s выдал @%s значок %s`%s`."},
{"id": "badges.notify.no_permission_channel", "translation": "У вас нет прав на отправку уведомления о выдаче в этот канал."}
]

8
server/manifest.go generated
View File

@ -13,8 +13,8 @@ var manifest *model.Manifest
const manifestStr = ` const manifestStr = `
{ {
"id": "ru.loop.plugin.achievements", "id": "ru.loop.plugin.achievements",
"name": "Badges for Mattermost", "name": "Achievements",
"description": "This plugin add badges support to Mattermost.", "description": "Плагин достижений и значков для Loop.",
"homepage_url": "https://github.com/larkox/mattermost-plugin-badges", "homepage_url": "https://github.com/larkox/mattermost-plugin-badges",
"support_url": "https://github.com/larkox/mattermost-plugin-badges/issues", "support_url": "https://github.com/larkox/mattermost-plugin-badges/issues",
"release_notes_url": "https://github.com/larkox/mattermost-plugin-badges/releases/tag/v0.2.1", "release_notes_url": "https://github.com/larkox/mattermost-plugin-badges/releases/tag/v0.2.1",
@ -38,9 +38,9 @@ const manifestStr = `
"settings": [ "settings": [
{ {
"key": "BadgesAdmin", "key": "BadgesAdmin",
"display_name": "Badges admin:", "display_name": "Администратор достижений:",
"type": "text", "type": "text",
"help_text": "This user will be considered as an admin for the badges plugin. They can create types, and modify and grant any badge.", "help_text": "Этот пользователь будет считаться администратором плагина достижений. Он может создавать типы, а также изменять и выдавать любые значки.",
"placeholder": "", "placeholder": "",
"default": null "default": null
} }

View File

@ -9,6 +9,8 @@ import (
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin" "github.com/mattermost/mattermost-server/v5/plugin"
"github.com/pkg/errors" "github.com/pkg/errors"
i18n "github.com/larkox/mattermost-plugin-badges/server/i18n"
) )
// Plugin implements the interface expected by the Mattermost server to communicate between the server and plugin processes. // Plugin implements the interface expected by the Mattermost server to communicate between the server and plugin processes.
@ -27,6 +29,11 @@ type Plugin struct {
store Store store Store
router *mux.Router router *mux.Router
badgeAdminUserID string badgeAdminUserID string
i18nBundle *i18n.Bundle
}
func (p *Plugin) getT(locale string) i18n.TranslationFunc {
return i18n.LocalizerFunc(p.i18nBundle, locale)
} }
// ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world. // ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world.
@ -41,15 +48,16 @@ func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req
func (p *Plugin) OnActivate() error { func (p *Plugin) OnActivate() error {
p.mm = pluginapi.NewClient(p.API) p.mm = pluginapi.NewClient(p.API)
botID, err := p.Helpers.EnsureBot(&model.Bot{ botID, err := p.Helpers.EnsureBot(&model.Bot{
Username: "badges", Username: "achievements",
DisplayName: "Badges Bot", DisplayName: "Achievements Bot",
Description: "Created by the Badges plugin.", Description: "Created by the Achievements plugin.",
}) })
if err != nil { if err != nil {
return errors.Wrap(err, "failed to ensure badges bot") return errors.Wrap(err, "failed to ensure badges bot")
} }
p.BotUserID = botID p.BotUserID = botID
p.store = NewStore(p.API) p.store = NewStore(p.API)
p.i18nBundle = i18n.Init()
p.initializeAPI() p.initializeAPI()
return p.mm.SlashCommand.Register(p.getCommand()) return p.mm.SlashCommand.Register(p.getCommand())

View File

@ -152,13 +152,15 @@ func (p *Plugin) notifyGrant(badgeID badgesmodel.BadgeID, granter string, grante
image = fmt.Sprintf("![icon](%s) ", b.Image) image = fmt.Sprintf("![icon](%s) ", b.Image)
} }
// DM to the granted user — use their locale
Tdm := p.getT(granted.Locale)
dmPost := &model.Post{} dmPost := &model.Post{}
dmText := fmt.Sprintf("@%s granted you the %s`%s` badge.", granterUser.Username, image, b.Name) dmText := Tdm("badges.notify.dm_text", "@%s выдал вам значок %s`%s`.", granterUser.Username, image, b.Name)
if reason != "" { if reason != "" {
dmText += "\nWhy? " + reason dmText += Tdm("badges.notify.dm_reason", "\nПочему? ") + reason
} }
dmAttachment := model.SlackAttachment{ dmAttachment := model.SlackAttachment{
Title: fmt.Sprintf("%sbadge granted!", image), Title: Tdm("badges.notify.title", "%sзначок выдан!", image),
Text: dmText, Text: dmText,
} }
model.ParseSlackAttachment(dmPost, []*model.SlackAttachment{&dmAttachment}) model.ParseSlackAttachment(dmPost, []*model.SlackAttachment{&dmAttachment})
@ -167,16 +169,18 @@ func (p *Plugin) notifyGrant(badgeID badgesmodel.BadgeID, granter string, grante
p.mm.Log.Debug("dm error", "err", err) p.mm.Log.Debug("dm error", "err", err)
} }
// Channel/subscription notifications — use granter's locale
Tch := p.getT(granterUser.Locale)
basePost := model.Post{ basePost := model.Post{
UserId: p.BotUserID, UserId: p.BotUserID,
ChannelId: channelID, ChannelId: channelID,
} }
text := fmt.Sprintf("@%s granted @%s the %s`%s` badge.", granterUser.Username, granted.Username, image, b.Name) text := Tch("badges.notify.channel_text", "@%s выдал @%s значок %s`%s`.", granterUser.Username, granted.Username, image, b.Name)
if reason != "" { if reason != "" {
text += "\nWhy? " + reason text += Tch("badges.notify.dm_reason", "\nПочему? ") + reason
} }
attachment := model.SlackAttachment{ attachment := model.SlackAttachment{
Title: fmt.Sprintf("%sbadge granted!", image), Title: Tch("badges.notify.title", "%sзначок выдан!", image),
Text: text, Text: text,
} }
model.ParseSlackAttachment(&basePost, []*model.SlackAttachment{&attachment}) model.ParseSlackAttachment(&basePost, []*model.SlackAttachment{&attachment})
@ -190,7 +194,8 @@ func (p *Plugin) notifyGrant(badgeID badgesmodel.BadgeID, granter string, grante
} }
if inChannel { if inChannel {
if !p.API.HasPermissionToChannel(granter, channelID, model.PERMISSION_CREATE_POST) { if !p.API.HasPermissionToChannel(granter, channelID, model.PERMISSION_CREATE_POST) {
p.mm.Post.SendEphemeralPost(granter, &model.Post{Message: "You don't have permissions to notify the grant on this channel.", ChannelId: channelID}) Tg := p.getT(granterUser.Locale)
p.mm.Post.SendEphemeralPost(granter, &model.Post{Message: Tg("badges.notify.no_permission_channel", "У вас нет прав на отправку уведомления о выдаче в этот канал."), ChannelId: channelID})
} else { } else {
post := basePost.Clone() post := basePost.Clone()
post.ChannelId = channelID post.ChannelId = channelID

Binary file not shown.

View File

@ -1 +1,40 @@
{} {
"badges.loading": "Loading...",
"badges.no_badges_yet": "No badges yet.",
"badges.badge_not_found": "Badge not found.",
"badges.user_not_found": "User not found.",
"badges.unknown": "unknown",
"badges.rhs.all_badges": "All badges",
"badges.rhs.my_badges": "My badges",
"badges.rhs.user_badges": "@{username}'s badges",
"badges.rhs.badge_details": "Badge Details",
"badges.label.type": "Type: {typeName}",
"badges.label.created_by": "Created by: {username}",
"badges.label.granted_by": "Granted by: {username}",
"badges.label.granted_at": "Granted at: {date}",
"badges.label.reason": "Why? {reason}",
"badges.granted.not_yet": "Not yet granted.",
"badges.granted.multiple": "Granted {times, plural, one {# time} other {# times}} to {users, plural, one {# user} other {# users}}.",
"badges.granted.single": "Granted to {users, plural, one {# user} other {# users}}.",
"badges.granted_to": "Granted to:",
"badges.set_status": "Set status to this badge",
"badges.grant_badge": "Grant badge",
"badges.and_more": "and {count} more. Click to see all.",
"badges.menu.open_list": "Open the list of all badges.",
"badges.menu.create_badge": "Create badge",
"badges.menu.create_type": "Create badge type",
"badges.menu.add_subscription": "Add badge subscription",
"badges.menu.remove_subscription": "Remove badge subscription",
"badges.sidebar.title": "Badges",
"badges.popover.title": "Badges",
"badges.admin.label": "Achievements Admin:",
"badges.admin.placeholder": "username",
"badges.admin.help_text": "This user will be considered the achievements plugin administrator. They can create types, as well as modify and grant any badges."
}

40
webapp/i18n/ru.json Normal file
View File

@ -0,0 +1,40 @@
{
"badges.loading": "Загрузка...",
"badges.no_badges_yet": "Значков пока нет.",
"badges.badge_not_found": "Значок не найден.",
"badges.user_not_found": "Пользователь не найден.",
"badges.unknown": "неизвестно",
"badges.rhs.all_badges": "Все значки",
"badges.rhs.my_badges": "Мои значки",
"badges.rhs.user_badges": "Значки @{username}",
"badges.rhs.badge_details": "Детали значка",
"badges.label.type": "Тип: {typeName}",
"badges.label.created_by": "Создал: {username}",
"badges.label.granted_by": "Выдал: {username}",
"badges.label.granted_at": "Выдан: {date}",
"badges.label.reason": "Причина: {reason}",
"badges.granted.not_yet": "Ещё не выдан.",
"badges.granted.multiple": "Выдан {times, plural, one {# раз} few {# раза} many {# раз} other {# раз}} {users, plural, one {# пользователю} few {# пользователям} many {# пользователям} other {# пользователям}}.",
"badges.granted.single": "Выдан {users, plural, one {# пользователю} few {# пользователям} many {# пользователям} other {# пользователям}}.",
"badges.granted_to": "Выдан:",
"badges.set_status": "Установить как статус",
"badges.grant_badge": "Выдать значок",
"badges.and_more": "и ещё {count}. Нажмите, чтобы увидеть все.",
"badges.menu.open_list": "Открыть список всех значков.",
"badges.menu.create_badge": "Создать значок",
"badges.menu.create_type": "Создать тип значков",
"badges.menu.add_subscription": "Добавить подписку на значки",
"badges.menu.remove_subscription": "Удалить подписку на значки",
"badges.sidebar.title": "Значки",
"badges.popover.title": "Значки",
"badges.admin.label": "Администратор достижений:",
"badges.admin.placeholder": "имя пользователя",
"badges.admin.help_text": "Этот пользователь будет считаться администратором плагина достижений. Он может создавать типы, а также изменять и выдавать любые значки."
}

View File

@ -58,6 +58,7 @@
"jest-canvas-mock": "2.3.1", "jest-canvas-mock": "2.3.1",
"jest-junit": "12.0.0", "jest-junit": "12.0.0",
"loop-plugin-sdk": "https://artifacts.wilix.dev/repository/npm-public-loop/loop-plugin-sdk/-/loop-plugin-sdk-0.1.6.tgz", "loop-plugin-sdk": "https://artifacts.wilix.dev/repository/npm-public-loop/loop-plugin-sdk/-/loop-plugin-sdk-0.1.6.tgz",
"react-intl": "6.8.9",
"sass": "1.86.0", "sass": "1.86.0",
"sass-loader": "11.0.1", "sass-loader": "11.0.1",
"style-loader": "2.0.0", "style-loader": "2.0.0",

View File

@ -0,0 +1,57 @@
/* eslint-disable react/prop-types */
import React, {useCallback} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
type Props = {
id: string;
value: string;
disabled: boolean;
onChange: (id: string, value: any) => void;
setSaveNeeded: () => void;
config: any;
license: any;
setByEnv: boolean;
registerSaveAction: (action: () => Promise<{error?: {message?: string}}>) => void;
unRegisterSaveAction: (action: () => Promise<{error?: {message?: string}}>) => void;
}
const BadgesAdminSetting: React.FC<Props> = ({id, value, disabled, onChange, setSaveNeeded}) => {
const intl = useIntl();
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onChange(id, e.target.value);
setSaveNeeded();
}, [id, onChange, setSaveNeeded]);
return (
<div className='form-group'>
<label className='control-label col-sm-4'>
<FormattedMessage
id='badges.admin.label'
defaultMessage='Администратор достижений:'
/>
</label>
<div className='col-sm-8'>
<input
className='form-control'
type='text'
value={value || ''}
disabled={disabled}
onChange={handleChange}
placeholder={intl.formatMessage({
id: 'badges.admin.placeholder',
defaultMessage: 'username',
})}
/>
<div className='help-text'>
<FormattedMessage
id='badges.admin.help_text'
defaultMessage='Этот пользователь будет считаться администратором плагина достижений. Он может создавать типы, а также изменять и выдавать любые значки.'
/>
</div>
</div>
</div>
);
};
export default BadgesAdminSetting;

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl';
import {systemEmojis} from 'mattermost-redux/actions/emojis'; import {systemEmojis} from 'mattermost-redux/actions/emojis';
import {BadgeID, AllBadgesBadge} from '../../types/badges'; import {BadgeID, AllBadgesBadge} from '../../types/badges';
@ -62,11 +64,21 @@ class AllBadges extends React.PureComponent<Props, State> {
render() { render() {
if (this.state.loading) { if (this.state.loading) {
return (<div className='AllBadges'>{'Loading...'}</div>); return (<div className='AllBadges'>
<FormattedMessage
id='badges.loading'
defaultMessage='Загрузка...'
/>
</div>);
} }
if (!this.state.badges || this.state.badges.length === 0) { if (!this.state.badges || this.state.badges.length === 0) {
return (<div className='AllBadges'>{'No badges yet.'}</div>); return (<div className='AllBadges'>
<FormattedMessage
id='badges.no_badges_yet'
defaultMessage='Значков пока нет.'
/>
</div>);
} }
const content = this.state.badges.map((badge) => { const content = this.state.badges.map((badge) => {
@ -80,7 +92,12 @@ class AllBadges extends React.PureComponent<Props, State> {
}); });
return ( return (
<div className='AllBadges'> <div className='AllBadges'>
<div><b>{'All badges'}</b></div> <div><b>
<FormattedMessage
id='badges.rhs.all_badges'
defaultMessage='Все значки'
/>
</b></div>
<RHSScrollbars>{content}</RHSScrollbars> <RHSScrollbars>{content}</RHSScrollbars>
</div> </div>
); );

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl';
import {AllBadgesBadge} from '../../types/badges'; import {AllBadgesBadge} from '../../types/badges';
import BadgeImage from '../utils/badge_image'; import BadgeImage from '../utils/badge_image';
import {markdown} from 'utils/markdown'; import {markdown} from 'utils/markdown';
@ -11,15 +13,32 @@ type Props = {
onClick: (badge: AllBadgesBadge) => void; onClick: (badge: AllBadgesBadge) => void;
} }
function getGrantedText(badge: AllBadgesBadge): string { function getGrantedText(badge: AllBadgesBadge): React.ReactNode {
if (badge.granted === 0) { if (badge.granted === 0) {
return 'Not yet granted.'; return (
<FormattedMessage
id='badges.granted.not_yet'
defaultMessage='Ещё не выдан.'
/>
);
} }
if (badge.multiple) { if (badge.multiple) {
return `Granted ${badge.granted_times} to ${badge.granted} users.`; return (
<FormattedMessage
id='badges.granted.multiple'
defaultMessage='Выдан {times, plural, one {# раз} few {# раза} many {# раз} other {# раз}} {users, plural, one {# пользователю} few {# пользователям} many {# пользователям} other {# пользователям}}.'
values={{times: badge.granted_times, users: badge.granted}}
/>
);
} }
return `Granted to ${badge.granted} users.`; return (
<FormattedMessage
id='badges.granted.single'
defaultMessage='Выдан {users, plural, one {# пользователю} few {# пользователям} many {# пользователям} other {# пользователям}}.'
values={{users: badge.granted}}
/>
);
} }
const AllBadgesRow: React.FC<Props> = ({badge, onClick}: Props) => { const AllBadgesRow: React.FC<Props> = ({badge, onClick}: Props) => {
@ -39,7 +58,13 @@ const AllBadgesRow: React.FC<Props> = ({badge, onClick}: Props) => {
<div> <div>
<div className='badge-name'>{badge.name}</div> <div className='badge-name'>{badge.name}</div>
<div className='badge-description'>{markdown(badge.description)}</div> <div className='badge-description'>{markdown(badge.description)}</div>
<div className='badge-type'>{'Type: ' + badge.type_name}</div> <div className='badge-type'>
<FormattedMessage
id='badges.label.type'
defaultMessage='Тип: {typeName}'
values={{typeName: badge.type_name}}
/>
</div>
<div className='granted-by'>{getGrantedText(badge)}</div> <div className='granted-by'>{getGrantedText(badge)}</div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl';
import {systemEmojis} from 'mattermost-redux/actions/emojis'; import {systemEmojis} from 'mattermost-redux/actions/emojis';
import {BadgeDetails, BadgeID} from '../../types/badges'; import {BadgeDetails, BadgeID} from '../../types/badges';
@ -87,15 +89,30 @@ class BadgeDetailsComponent extends React.PureComponent<Props, State> {
render() { render() {
const {badge, loading} = this.state; const {badge, loading} = this.state;
if (this.props.badgeID == null) { if (this.props.badgeID == null) {
return (<div>{'Badge not found.'}</div>); return (<div>
<FormattedMessage
id='badges.badge_not_found'
defaultMessage='Значок не найден.'
/>
</div>);
} }
if (loading) { if (loading) {
return (<div>{'Loading...'}</div>); return (<div>
<FormattedMessage
id='badges.loading'
defaultMessage='Загрузка...'
/>
</div>);
} }
if (!badge) { if (!badge) {
return (<div>{'Badge not found.'}</div>); return (<div>
<FormattedMessage
id='badges.badge_not_found'
defaultMessage='Значок не найден.'
/>
</div>);
} }
const content = badge.owners.map((ownership) => { const content = badge.owners.map((ownership) => {
@ -109,7 +126,12 @@ class BadgeDetailsComponent extends React.PureComponent<Props, State> {
}); });
return ( return (
<div className='BadgeDetails'> <div className='BadgeDetails'>
<div><b>{'Badge Details'}</b></div> <div><b>
<FormattedMessage
id='badges.rhs.badge_details'
defaultMessage='Детали значка'
/>
</b></div>
<div className='badge-info'> <div className='badge-info'>
<span className='badge-icon'> <span className='badge-icon'>
<BadgeImage <BadgeImage
@ -120,11 +142,28 @@ class BadgeDetailsComponent extends React.PureComponent<Props, State> {
<div className='badge-text'> <div className='badge-text'>
<div className='badge-name'>{badge.name}</div> <div className='badge-name'>{badge.name}</div>
<div className='badge-description'>{markdown(badge.description)}</div> <div className='badge-description'>{markdown(badge.description)}</div>
<div className='badge-type'>{'Type: ' + badge.type_name}</div> <div className='badge-type'>
<div className='created-by'>{`Created by: ${badge.created_by_username}`}</div> <FormattedMessage
id='badges.label.type'
defaultMessage='Тип: {typeName}'
values={{typeName: badge.type_name}}
/>
</div>
<div className='created-by'>
<FormattedMessage
id='badges.label.created_by'
defaultMessage='Создал: {username}'
values={{username: badge.created_by_username}}
/>
</div>
</div> </div>
</div> </div>
<div><b>{'Granted to:'}</b></div> <div><b>
<FormattedMessage
id='badges.granted_to'
defaultMessage='Выдан:'
/>
</b></div>
<RHSScrollbars>{content}</RHSScrollbars> <RHSScrollbars>{content}</RHSScrollbars>
</div> </div>
); );

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl';
import Client4 from 'mattermost-redux/client/client4'; import Client4 from 'mattermost-redux/client/client4';
import {UserBadge} from '../../types/badges'; import {UserBadge} from '../../types/badges';
@ -18,7 +20,15 @@ const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser}: Props) =
const time = new Date(badge.time); const time = new Date(badge.time);
let reason = null; let reason = null;
if (badge.reason) { if (badge.reason) {
reason = (<div className='badge-user-reason'>{'Why? ' + badge.reason}</div>); reason = (
<div className='badge-user-reason'>
<FormattedMessage
id='badges.label.reason'
defaultMessage='Причина: {reason}'
values={{reason: badge.reason}}
/>
</div>
);
} }
let setStatus = null; let setStatus = null;
if (isCurrentUser && badge.image_type === 'emoji') { if (isCurrentUser && badge.image_type === 'emoji') {
@ -30,7 +40,10 @@ const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser}: Props) =
c.updateCustomStatus({emoji: badge.image, text: badge.name}); c.updateCustomStatus({emoji: badge.image, text: badge.name});
}} }}
> >
{'Set status to this badge'} <FormattedMessage
id='badges.set_status'
defaultMessage='Установить как статус'
/>
</a> </a>
</div> </div>
); );
@ -49,9 +62,27 @@ const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser}: Props) =
<div className='user-badge-name'>{badge.name}</div> <div className='user-badge-name'>{badge.name}</div>
<div className='user-badge-description'>{markdown(badge.description)}</div> <div className='user-badge-description'>{markdown(badge.description)}</div>
{reason} {reason}
<div className='user-badge-type'>{'Type: ' + badge.type_name}</div> <div className='user-badge-type'>
<div className='user-badge-granted-by'>{`Granted by: ${badge.granted_by_name}`}</div> <FormattedMessage
<div className='user-badge-granted-at'>{`Granted at: ${time.toDateString()}`}</div> id='badges.label.type'
defaultMessage='Тип: {typeName}'
values={{typeName: badge.type_name}}
/>
</div>
<div className='user-badge-granted-by'>
<FormattedMessage
id='badges.label.granted_by'
defaultMessage='Выдал: {username}'
values={{username: badge.granted_by_name}}
/>
</div>
<div className='user-badge-granted-at'>
<FormattedMessage
id='badges.label.granted_at'
defaultMessage='Выдан: {date}'
values={{date: time.toDateString()}}
/>
</div>
{setStatus} {setStatus}
</div> </div>
</div> </div>

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl';
import {UserProfile} from 'mattermost-redux/types/users'; import {UserProfile} from 'mattermost-redux/types/users';
import {systemEmojis} from 'mattermost-redux/actions/emojis'; import {systemEmojis} from 'mattermost-redux/actions/emojis';
@ -84,15 +86,30 @@ class UserBadges extends React.PureComponent<Props, State> {
render() { render() {
if (!this.props.user) { if (!this.props.user) {
return (<div>{'User not found.'}</div>); return (<div>
<FormattedMessage
id='badges.user_not_found'
defaultMessage='Пользователь не найден.'
/>
</div>);
} }
if (this.state.loading) { if (this.state.loading) {
return (<div>{'Loading...'}</div>); return (<div>
<FormattedMessage
id='badges.loading'
defaultMessage='Загрузка...'
/>
</div>);
} }
if (!this.state.badges || this.state.badges.length === 0) { if (!this.state.badges || this.state.badges.length === 0) {
return (<div>{'No badges yet.'}</div>); return (<div>
<FormattedMessage
id='badges.no_badges_yet'
defaultMessage='Значков пока нет.'
/>
</div>);
} }
const content = this.state.badges.map((badge) => { const content = this.state.badges.map((badge) => {
@ -106,10 +123,18 @@ class UserBadges extends React.PureComponent<Props, State> {
); );
}); });
let title = 'My badges'; const title = this.props.isCurrentUser ? (
if (!this.props.isCurrentUser) { <FormattedMessage
title = `@${this.props.user.username}'s badges`; id='badges.rhs.my_badges'
} defaultMessage='Мои значки'
/>
) : (
<FormattedMessage
id='badges.rhs.user_badges'
defaultMessage='Значки @{username}'
values={{username: this.props.user.username}}
/>
);
return ( return (
<div className='UserBadges'> <div className='UserBadges'>
<div><b>{title}</b></div> <div><b>{title}</b></div>

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useSelector} from 'react-redux'; import {useSelector} from 'react-redux';
import {getUser} from 'mattermost-redux/selectors/entities/users'; import {getUser} from 'mattermost-redux/selectors/entities/users';
import {GlobalState} from 'mattermost-redux/types/store'; import {GlobalState} from 'mattermost-redux/types/store';
@ -14,6 +16,7 @@ type Props = {
} }
const UserBadgeRow: React.FC<Props> = ({ownership, onClick}: Props) => { const UserBadgeRow: React.FC<Props> = ({ownership, onClick}: Props) => {
const intl = useIntl();
const user = useSelector<GlobalState, UserProfile>((state) => getUser(state, ownership.user)); const user = useSelector<GlobalState, UserProfile>((state) => getUser(state, ownership.user));
const grantedBy = useSelector<GlobalState, UserProfile>((state) => getUser(state, ownership.granted_by)); const grantedBy = useSelector<GlobalState, UserProfile>((state) => getUser(state, ownership.granted_by));
@ -21,7 +24,7 @@ const UserBadgeRow: React.FC<Props> = ({ownership, onClick}: Props) => {
return null; return null;
} }
let grantedByName = 'unknown'; let grantedByName = intl.formatMessage({id: 'badges.unknown', defaultMessage: 'неизвестно'});
if (grantedBy) { if (grantedBy) {
grantedByName = '@' + grantedBy.username; grantedByName = '@' + grantedBy.username;
} }
@ -30,8 +33,20 @@ const UserBadgeRow: React.FC<Props> = ({ownership, onClick}: Props) => {
return ( return (
<div className='UserRow'> <div className='UserRow'>
<div className='badge-user-username'><a onClick={() => onClick(ownership.user)}>{`@${user.username}`}</a></div> <div className='badge-user-username'><a onClick={() => onClick(ownership.user)}>{`@${user.username}`}</a></div>
<div className='badge-user-granted-by'>{`Granted by: ${grantedByName}`}</div> <div className='badge-user-granted-by'>
<div className='badge-user-granted-at'>{`Granted at: ${time.toDateString()}`}</div> <FormattedMessage
id='badges.label.granted_by'
defaultMessage='Выдал: {username}'
values={{username: grantedByName}}
/>
</div>
<div className='badge-user-granted-at'>
<FormattedMessage
id='badges.label.granted_at'
defaultMessage='Выдан: {date}'
values={{date: time.toDateString()}}
/>
</div>
</div> </div>
); );
}; };

View File

@ -1,7 +1,7 @@
import {UserProfile} from 'mattermost-redux/types/users'; import {UserProfile} from 'mattermost-redux/types/users';
import React from 'react'; import React from 'react';
import {Tooltip, OverlayTrigger} from 'react-bootstrap'; import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import {GlobalState} from 'mattermost-redux/types/store'; import {GlobalState} from 'mattermost-redux/types/store';
@ -10,13 +10,13 @@ import {systemEmojis} from 'mattermost-redux/actions/emojis';
import {BadgeID, UserBadge} from 'types/badges'; import {BadgeID, UserBadge} from 'types/badges';
import Client from 'client/api'; import Client from 'client/api';
import BadgeImage from '../utils/badge_image'; import BadgeImage from '../utils/badge_image';
import TooltipWrapper from '../utils/tooltip_wrapper';
import {RHSState} from 'types/general'; import {RHSState} from 'types/general';
import {IMAGE_TYPE_EMOJI, RHS_STATE_DETAIL, RHS_STATE_MY, RHS_STATE_OTHER} from '../../constants'; import {IMAGE_TYPE_EMOJI, RHS_STATE_DETAIL, RHS_STATE_MY, RHS_STATE_OTHER} from '../../constants';
import {markdown} from 'utils/markdown';
import './badge_list.scss'; import './badge_list.scss';
type Props = { type Props = {
intl: IntlShape;
debug: GlobalState; debug: GlobalState;
user: UserProfile; user: UserProfile;
currentUserID: string; currentUserID: string;
@ -104,6 +104,7 @@ class BadgeList extends React.PureComponent<Props, State> {
} }
render() { render() {
const {intl} = this.props;
const nBadges = this.state.badges?.length || 0; const nBadges = this.state.badges?.length || 0;
const toShow = nBadges < MAX_BADGES ? nBadges : MAX_BADGES; const toShow = nBadges < MAX_BADGES ? nBadges : MAX_BADGES;
@ -111,47 +112,55 @@ class BadgeList extends React.PureComponent<Props, State> {
for (let i = 0; i < toShow; i++) { for (let i = 0; i < toShow; i++) {
const badge = this.state.badges![i]; const badge = this.state.badges![i];
const time = new Date(badge.time); const time = new Date(badge.time);
let reason = null; let reason: string | null = null;
if (badge.reason) { if (badge.reason) {
reason = (<div>{'Why? ' + badge.reason}</div>); reason = intl.formatMessage(
{id: 'badges.label.reason', defaultMessage: 'Причина: {reason}'},
{reason: badge.reason},
);
} }
const grantedBy = intl.formatMessage(
{id: 'badges.label.granted_by', defaultMessage: 'Выдал: {username}'},
{username: badge.granted_by_name},
);
const grantedAt = intl.formatMessage(
{id: 'badges.label.granted_at', defaultMessage: 'Выдан: {date}'},
{date: time.toDateString()},
);
const tooltipLines = [
badge.name,
badge.description,
reason,
grantedBy,
grantedAt,
].filter(Boolean).join('\n');
const badgeComponent = ( const badgeComponent = (
<OverlayTrigger <TooltipWrapper tooltipContent={tooltipLines}>
overlay={<Tooltip id='badgeTooltip'> <a onClick={() => this.onBadgeClick(badge)}>
<div>{badge.name}</div> <BadgeImage
<div>{markdown(badge.description)}</div> badge={badge}
{reason} size={BADGE_SIZE}
<div>{`Granted by: ${badge.granted_by_name}`}</div> />
<div>{`Granted at: ${time.toDateString()}`}</div> </a>
</Tooltip>} </TooltipWrapper>
>
<span>
<a onClick={() => this.onBadgeClick(badge)}>
<BadgeImage
badge={badge}
size={BADGE_SIZE}
/>
</a>
</span>
</OverlayTrigger>
); );
content.push(badgeComponent); content.push(badgeComponent);
} }
let andMore: React.ReactNode = null; let andMore: React.ReactNode = null;
if (nBadges > MAX_BADGES) { if (nBadges > MAX_BADGES) {
const andMoreText = intl.formatMessage(
{id: 'badges.and_more', defaultMessage: 'и ещё {count}. Нажмите, чтобы увидеть все.'},
{count: nBadges - MAX_BADGES},
);
andMore = ( andMore = (
<OverlayTrigger <TooltipWrapper tooltipContent={andMoreText}>
overlay={<Tooltip id='badgeMoreTooltip'>
{`and ${nBadges - MAX_BADGES} more. Click to see all.`}
</Tooltip>}
>
<button <button
id='showMoreButton' id='showMoreButton'
onClick={this.onMoreClick} onClick={this.onMoreClick}
> >
<span className={'fa fa-angle-right'}/> <span className={'fa fa-angle-right'}/>
</button> </button>
</OverlayTrigger> </TooltipWrapper>
); );
} }
const maxWidth = (MAX_BADGES * BADGE_SIZE) + 30; const maxWidth = (MAX_BADGES * BADGE_SIZE) + 30;
@ -161,13 +170,21 @@ class BadgeList extends React.PureComponent<Props, State> {
// Reserve enough height one row of badges and the "and more" button // Reserve enough height one row of badges and the "and more" button
<div style={{height: BADGE_SIZE, minWidth: 66, maxWidth}}> <div style={{height: BADGE_SIZE, minWidth: 66, maxWidth}}>
{'Loading...'} <FormattedMessage
id='badges.loading'
defaultMessage='Загрузка...'
/>
</div> </div>
); );
} }
return ( return (
<div id='badgePlugin'> <div id='badgePlugin'>
<div><b>{'Badges'}</b></div> <div><b>
<FormattedMessage
id='badges.popover.title'
defaultMessage='Значки'
/>
</b></div>
<div id='contentContainer' > <div id='contentContainer' >
{content} {content}
{andMore} {andMore}
@ -178,7 +195,10 @@ class BadgeList extends React.PureComponent<Props, State> {
onClick={this.onGrantClick} onClick={this.onGrantClick}
> >
<span className={'fa fa-plus-circle'}/> <span className={'fa fa-plus-circle'}/>
{'Grant badge'} <FormattedMessage
id='badges.grant_badge'
defaultMessage='Выдать значок'
/>
</button> </button>
<hr className='divider divider--expanded'/> <hr className='divider divider--expanded'/>
</div> </div>
@ -186,4 +206,4 @@ class BadgeList extends React.PureComponent<Props, State> {
} }
} }
export default BadgeList; export default injectIntl(BadgeList);

View File

@ -0,0 +1,31 @@
.badge-tooltip-wrapper {
position: relative;
display: inline-block;
}
.badge-tooltip {
position: absolute;
background-color: #000;
width: max-content;
color: #fff;
font-size: 12px;
font-weight: 600;
text-align: center;
border-radius: 4px;
padding: 4px 8px;
z-index: 10000;
max-width: 220px;
overflow-wrap: break-word;
pointer-events: none;
transition: opacity 0.2s ease;
white-space: pre-line;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.12);
}
.badge-tooltip-arrow {
position: absolute;
width: 8px;
height: 8px;
background: #000;
transform: rotate(45deg);
}

View File

@ -0,0 +1,69 @@
/* eslint-disable react/prop-types */
import React, {ReactNode, useState, useRef, useEffect} from 'react';
import ReactDOM from 'react-dom';
import './tooltip_wrapper.scss';
type TooltipWrapperProps = {
children: ReactNode;
tooltipContent: ReactNode;
}
const TooltipWrapper: React.FC<TooltipWrapperProps> = ({children, tooltipContent}) => {
const [isVisible, setIsVisible] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({bottom: 0, left: 0});
const handleMouseEnter = () => setIsVisible(true);
const handleMouseLeave = () => setIsVisible(false);
useEffect(() => {
if (wrapperRef.current && isVisible) {
const wrapperRect = wrapperRef.current.getBoundingClientRect();
setPosition({
bottom: window.innerHeight - (wrapperRect.top + window.scrollY - 5),
left: wrapperRect.left + window.scrollX + wrapperRect.width / 2,
});
}
}, [isVisible]);
const tooltip = (
<div
ref={tooltipRef}
className='badge-tooltip'
style={{
position: 'absolute',
bottom: `${position.bottom}px`,
left: `${position.left}px`,
transform: 'translateX(-50%)',
visibility: isVisible ? 'visible' : 'hidden',
opacity: isVisible ? 1 : 0,
}}
>
{tooltipContent}
<div
className='badge-tooltip-arrow'
style={{
bottom: '-4px',
left: '50%',
marginLeft: '-4px',
}}
/>
</div>
);
return (
<div
ref={wrapperRef}
className='badge-tooltip-wrapper'
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
{isVisible && ReactDOM.createPortal(tooltip as any, document.body)}
</div>
);
};
export default TooltipWrapper;

View File

@ -6,11 +6,17 @@ import {GenericAction} from 'mattermost-redux/types/actions';
import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/common';
import React from 'react'; import React from 'react';
import {IntlProvider} from 'react-intl';
import {useSelector} from 'react-redux';
import {openAddSubscription, openCreateBadge, openCreateType, openRemoveSubscription, setRHSView, setShowRHSAction} from 'actions/actions'; import {openAddSubscription, openCreateBadge, openCreateType, openRemoveSubscription, setRHSView, setShowRHSAction} from 'actions/actions';
import UserBadges from 'components/rhs'; import RHSComponent from 'components/rhs';
import ChannelHeaderButton from 'components/channel_header_button'; import ChannelHeaderButton from 'components/channel_header_button';
@ -20,28 +26,55 @@ import manifest from './manifest';
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import {PluginRegistry} from './types/mattermost-webapp'; import {PluginRegistry} from './types/mattermost-webapp';
import BadgeList from './components/user_popover/'; import BadgeListConnected from './components/user_popover/';
import {RHS_STATE_ALL} from './constants'; import {RHS_STATE_ALL} from './constants';
import {getTranslations} from './utils/i18n';
import BadgesAdminSetting from './components/admin/badges_admin_setting';
function withIntl(Component: React.ElementType): React.ElementType {
const Wrapped: React.FC<any> = (props) => {
const currentUser = useSelector(getCurrentUser);
const locale = currentUser?.locale || 'ru';
return (
<IntlProvider
locale={locale}
messages={getTranslations(locale)}
>
<Component {...props}/>
</IntlProvider>
);
};
return Wrapped;
}
const WrappedRHS = withIntl(RHSComponent);
const WrappedBadgeList = withIntl(BadgeListConnected as unknown as React.ElementType);
export default class Plugin { export default class Plugin {
public async initialize(registry: PluginRegistry, store: Store<GlobalState, GenericAction>) { public async initialize(registry: PluginRegistry, store: Store<GlobalState, GenericAction>) {
registry.registerReducer(Reducer); registry.registerReducer(Reducer);
registry.registerPopoverUserAttributesComponent(BadgeList); registry.registerTranslations(getTranslations);
const {showRHSPlugin, toggleRHSPlugin} = registry.registerRightHandSidebarComponent(UserBadges, 'Badges'); registry.registerAdminConsoleCustomSetting('BadgesAdmin', withIntl(BadgesAdminSetting));
registry.registerPopoverUserAttributesComponent(WrappedBadgeList);
const locale = getCurrentUser(store.getState())?.locale || 'ru';
const messages = getTranslations(locale);
const {showRHSPlugin, toggleRHSPlugin} = registry.registerRightHandSidebarComponent(WrappedRHS, messages['badges.sidebar.title']);
store.dispatch(setShowRHSAction(() => store.dispatch(showRHSPlugin))); store.dispatch(setShowRHSAction(() => store.dispatch(showRHSPlugin)));
const toggleRHS = () => { const toggleRHS = () => {
store.dispatch(setRHSView(RHS_STATE_ALL)); store.dispatch(setRHSView(RHS_STATE_ALL));
store.dispatch(toggleRHSPlugin); store.dispatch(toggleRHSPlugin);
} };
registry.registerChannelHeaderButtonAction( registry.registerChannelHeaderButtonAction(
<ChannelHeaderButton/>, <ChannelHeaderButton/>,
toggleRHS, toggleRHS,
'Badges', messages['badges.sidebar.title'],
'Open the list of all badges.', messages['badges.menu.open_list'],
); );
if (registry.registerAppBarComponent) { if (registry.registerAppBarComponent) {
@ -50,19 +83,19 @@ export default class Plugin {
registry.registerAppBarComponent( registry.registerAppBarComponent(
iconURL, iconURL,
toggleRHS, toggleRHS,
'Open the list of all badges.', messages['badges.menu.open_list'],
); );
} }
registry.registerMainMenuAction( registry.registerMainMenuAction(
'Create badge', messages['badges.menu.create_badge'],
() => { () => {
store.dispatch(openCreateBadge() as any); store.dispatch(openCreateBadge() as any);
}, },
null, null,
); );
registry.registerMainMenuAction( registry.registerMainMenuAction(
'Create badge type', messages['badges.menu.create_type'],
() => { () => {
store.dispatch(openCreateType() as any); store.dispatch(openCreateType() as any);
}, },
@ -70,13 +103,13 @@ export default class Plugin {
); );
registry.registerChannelHeaderMenuAction( registry.registerChannelHeaderMenuAction(
'Add badge subscription', messages['badges.menu.add_subscription'],
() => { () => {
store.dispatch(openAddSubscription() as any); store.dispatch(openAddSubscription() as any);
}, },
); );
registry.registerChannelHeaderMenuAction( registry.registerChannelHeaderMenuAction(
'Remove badge subscription', messages['badges.menu.remove_subscription'],
() => { () => {
store.dispatch(openRemoveSubscription() as any); store.dispatch(openRemoveSubscription() as any);
}, },

View File

@ -11,7 +11,9 @@ export interface PluginRegistry {
registerChannelHeaderButtonAction(icon: React.ReactNode, action: () => void, dropdownText: string, tooltip: string); registerChannelHeaderButtonAction(icon: React.ReactNode, action: () => void, dropdownText: string, tooltip: string);
registerMainMenuAction(text: React.ReactNode, action: () => void, mobileIcon: React.ReactNode); registerMainMenuAction(text: React.ReactNode, action: () => void, mobileIcon: React.ReactNode);
registerChannelHeaderMenuAction(text: string, action: (channelID: string) => void); registerChannelHeaderMenuAction(text: string, action: (channelID: string) => void);
registerAppBarComponent(iconURL: string, action: (channel: Channel, member: ChannelMembership) => void, tooltipText: React.ReactNode) registerAppBarComponent(iconURL: string, action: (channel: Channel, member: ChannelMembership) => void, tooltipText: React.ReactNode);
registerTranslations(getTranslationsForLocale: (locale: string) => Record<string, string>): void;
registerAdminConsoleCustomSetting(key: string, component: React.ElementType, options?: {showTitle: boolean}): void;
// Add more if needed from https://developers.mattermost.com/extend/plugins/webapp/reference // Add more if needed from https://developers.mattermost.com/extend/plugins/webapp/reference
} }

8
webapp/src/utils/i18n.ts Normal file
View File

@ -0,0 +1,8 @@
import en from '../../i18n/en.json';
import ru from '../../i18n/ru.json';
const translations: Record<string, Record<string, string>> = {en, ru};
export function getTranslations(locale: string): Record<string, string> {
return translations[locale] || translations.ru;
}

View File

@ -96,6 +96,7 @@ module.exports = {
}, },
externals: { externals: {
react: 'React', react: 'React',
'react-dom': 'ReactDOM',
redux: 'Redux', redux: 'Redux',
'react-redux': 'ReactRedux', 'react-redux': 'ReactRedux',
'prop-types': 'PropTypes', 'prop-types': 'PropTypes',

View File

@ -1606,6 +1606,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/ecma402-abstract@npm:2.2.4":
version: 2.2.4
resolution: "@formatjs/ecma402-abstract@npm:2.2.4"
dependencies:
"@formatjs/fast-memoize": "npm:2.2.3"
"@formatjs/intl-localematcher": "npm:0.5.8"
tslib: "npm:2"
checksum: 10c0/3f262533fa704ea7a1a7a8107deee2609774a242c621f8cb5dd4bf4c97abf2fc12f5aeda3f4ce85be18147c484a0ca87303dca6abef53290717e685c55eabd2d
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:3.1.1": "@formatjs/ecma402-abstract@npm:3.1.1":
version: 3.1.1 version: 3.1.1
resolution: "@formatjs/ecma402-abstract@npm:3.1.1" resolution: "@formatjs/ecma402-abstract@npm:3.1.1"
@ -1618,6 +1629,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/fast-memoize@npm:2.2.3":
version: 2.2.3
resolution: "@formatjs/fast-memoize@npm:2.2.3"
dependencies:
tslib: "npm:2"
checksum: 10c0/f1004c3b280de7e362bd37c5f48ff34c2ba1d6271d4a7b695fed561d1201a3379397824d8bffbf15fecee344d1e70398393bbb04297f242692310a305f12e75b
languageName: node
linkType: hard
"@formatjs/fast-memoize@npm:3.1.0": "@formatjs/fast-memoize@npm:3.1.0":
version: 3.1.0 version: 3.1.0
resolution: "@formatjs/fast-memoize@npm:3.1.0" resolution: "@formatjs/fast-memoize@npm:3.1.0"
@ -1627,6 +1647,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/icu-messageformat-parser@npm:2.9.4":
version: 2.9.4
resolution: "@formatjs/icu-messageformat-parser@npm:2.9.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.4"
"@formatjs/icu-skeleton-parser": "npm:1.8.8"
tslib: "npm:2"
checksum: 10c0/f1ed14ece7ef0abc9fb62e323b78c994fc772d346801ad5aaa9555e1a7d5c0fda791345f4f2e53a3223f0b82c1a4eaf9a83544c1c20cb39349d1a39bedcf1648
languageName: node
linkType: hard
"@formatjs/icu-messageformat-parser@npm:3.5.1": "@formatjs/icu-messageformat-parser@npm:3.5.1":
version: 3.5.1 version: 3.5.1
resolution: "@formatjs/icu-messageformat-parser@npm:3.5.1" resolution: "@formatjs/icu-messageformat-parser@npm:3.5.1"
@ -1638,6 +1669,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/icu-skeleton-parser@npm:1.8.8":
version: 1.8.8
resolution: "@formatjs/icu-skeleton-parser@npm:1.8.8"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.4"
tslib: "npm:2"
checksum: 10c0/5ad78a5682e83b973e6fed4fca68660b944c41d1e941f0c84d69ff3d10ae835330062dc0a2cf0d237d2675ad3463405061a3963c14c2b9d8d1c1911f892b1a8d
languageName: node
linkType: hard
"@formatjs/icu-skeleton-parser@npm:2.1.1": "@formatjs/icu-skeleton-parser@npm:2.1.1":
version: 2.1.1 version: 2.1.1
resolution: "@formatjs/icu-skeleton-parser@npm:2.1.1" resolution: "@formatjs/icu-skeleton-parser@npm:2.1.1"
@ -1648,6 +1689,37 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-displaynames@npm:6.8.5":
version: 6.8.5
resolution: "@formatjs/intl-displaynames@npm:6.8.5"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.4"
"@formatjs/intl-localematcher": "npm:0.5.8"
tslib: "npm:2"
checksum: 10c0/1092d6bac9ba7ee22470b85c9af16802244aa8a54f07e6cd560d15b96e8a08fc359f20dee88a064fe4c9ca8860f439abb109cbb7977b9ccceb846e28aacdf29c
languageName: node
linkType: hard
"@formatjs/intl-listformat@npm:7.7.5":
version: 7.7.5
resolution: "@formatjs/intl-listformat@npm:7.7.5"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.4"
"@formatjs/intl-localematcher": "npm:0.5.8"
tslib: "npm:2"
checksum: 10c0/f514397f6b05ac29171fffbbd15636fbec086080058c79c159f24edd2038747c22579d46ebf339cbb672f8505ea408e5d960d6751064c16e02d18445cf4e7e61
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.5.8":
version: 0.5.8
resolution: "@formatjs/intl-localematcher@npm:0.5.8"
dependencies:
tslib: "npm:2"
checksum: 10c0/7a660263986326b662d4cb537e8386331c34fda61fb830b105e6c62d49be58ace40728dae614883b27a41cec7b1df8b44f72f79e16e6028bfca65d398dc04f3b
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.8.1": "@formatjs/intl-localematcher@npm:0.8.1":
version: 0.8.1 version: 0.8.1
resolution: "@formatjs/intl-localematcher@npm:0.8.1" resolution: "@formatjs/intl-localematcher@npm:0.8.1"
@ -1658,6 +1730,26 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl@npm:2.10.15":
version: 2.10.15
resolution: "@formatjs/intl@npm:2.10.15"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.4"
"@formatjs/fast-memoize": "npm:2.2.3"
"@formatjs/icu-messageformat-parser": "npm:2.9.4"
"@formatjs/intl-displaynames": "npm:6.8.5"
"@formatjs/intl-listformat": "npm:7.7.5"
intl-messageformat: "npm:10.7.7"
tslib: "npm:2"
peerDependencies:
typescript: ^4.7 || 5
peerDependenciesMeta:
typescript:
optional: true
checksum: 10c0/5d51fd0785d5547f375991d7df2d6303479b0083eeb35c42c30c9633aab77101895498f1eace419fd34fdb5c84aea19037c5280c3a9d85f9c3ffe6eef76b6f39
languageName: node
linkType: hard
"@formatjs/intl@npm:4.1.2": "@formatjs/intl@npm:4.1.2":
version: 4.1.2 version: 4.1.2
resolution: "@formatjs/intl@npm:4.1.2" resolution: "@formatjs/intl@npm:4.1.2"
@ -2353,7 +2445,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/hoist-non-react-statics@npm:^3.3.0, @types/hoist-non-react-statics@npm:^3.3.1": "@types/hoist-non-react-statics@npm:3, @types/hoist-non-react-statics@npm:^3.3.0, @types/hoist-non-react-statics@npm:^3.3.1":
version: 3.3.7 version: 3.3.7
resolution: "@types/hoist-non-react-statics@npm:3.3.7" resolution: "@types/hoist-non-react-statics@npm:3.3.7"
dependencies: dependencies:
@ -2556,6 +2648,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/react@npm:16 || 17 || 18":
version: 18.3.28
resolution: "@types/react@npm:18.3.28"
dependencies:
"@types/prop-types": "npm:*"
csstype: "npm:^3.2.2"
checksum: 10c0/683e19cd12b5c691215529af2e32b5ffbaccae3bf0ba93bfafa0e460e8dfee18423afed568be2b8eadf4b837c3749dd296a4f64e2d79f68fa66962c05f5af661
languageName: node
linkType: hard
"@types/react@npm:17.0.3": "@types/react@npm:17.0.3":
version: 17.0.3 version: 17.0.3
resolution: "@types/react@npm:17.0.3" resolution: "@types/react@npm:17.0.3"
@ -6301,7 +6403,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": "hoist-non-react-statics@npm:3, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2":
version: 3.3.2 version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2" resolution: "hoist-non-react-statics@npm:3.3.2"
dependencies: dependencies:
@ -6566,6 +6668,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"intl-messageformat@npm:10.7.7":
version: 10.7.7
resolution: "intl-messageformat@npm:10.7.7"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.4"
"@formatjs/fast-memoize": "npm:2.2.3"
"@formatjs/icu-messageformat-parser": "npm:2.9.4"
tslib: "npm:2"
checksum: 10c0/691895fb6a73a2feb2569658706e0d452861441de184dd1c9201e458a39fb80fc80080dd40d3d370400a52663f87de7a6d5a263c94245492f7265dd760441a95
languageName: node
linkType: hard
"intl-messageformat@npm:11.1.2": "intl-messageformat@npm:11.1.2":
version: 11.1.2 version: 11.1.2
resolution: "intl-messageformat@npm:11.1.2" resolution: "intl-messageformat@npm:11.1.2"
@ -9460,6 +9574,30 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-intl@npm:6.8.9":
version: 6.8.9
resolution: "react-intl@npm:6.8.9"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.2.4"
"@formatjs/icu-messageformat-parser": "npm:2.9.4"
"@formatjs/intl": "npm:2.10.15"
"@formatjs/intl-displaynames": "npm:6.8.5"
"@formatjs/intl-listformat": "npm:7.7.5"
"@types/hoist-non-react-statics": "npm:3"
"@types/react": "npm:16 || 17 || 18"
hoist-non-react-statics: "npm:3"
intl-messageformat: "npm:10.7.7"
tslib: "npm:2"
peerDependencies:
react: ^16.6.0 || 17 || 18
typescript: ^4.7 || 5
peerDependenciesMeta:
typescript:
optional: true
checksum: 10c0/d42a6252beac5448b4a248d84923b0f75dfbbee6208cd5c49ac2f525714ab94efe2a4933d464c64cb161ddccaa37b83dffb2dd0529428219b8a60ce548da3e57
languageName: node
linkType: hard
"react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.7.0, react-is@npm:^16.8.6": "react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.7.0, react-is@npm:^16.8.6":
version: 16.13.1 version: 16.13.1
resolution: "react-is@npm:16.13.1" resolution: "react-is@npm:16.13.1"
@ -10090,6 +10228,7 @@ __metadata:
memoize-one: "npm:^5.2.1" memoize-one: "npm:^5.2.1"
react: "npm:17.0.2" react: "npm:17.0.2"
react-custom-scrollbars: "npm:^4.2.1" react-custom-scrollbars: "npm:^4.2.1"
react-intl: "npm:6.8.9"
react-redux: "npm:7.2.3" react-redux: "npm:7.2.3"
redux: "npm:4.0.5" redux: "npm:4.0.5"
sass: "npm:1.86.0" sass: "npm:1.86.0"
@ -11364,6 +11503,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tslib@npm:2, tslib@npm:^2.8.1":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
languageName: node
linkType: hard
"tslib@npm:^1.8.1": "tslib@npm:^1.8.1":
version: 1.14.1 version: 1.14.1
resolution: "tslib@npm:1.14.1" resolution: "tslib@npm:1.14.1"
@ -11371,13 +11517,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tslib@npm:^2.8.1":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
languageName: node
linkType: hard
"tsutils@npm:^3.17.1": "tsutils@npm:^3.17.1":
version: 3.21.0 version: 3.21.0
resolution: "tsutils@npm:3.21.0" resolution: "tsutils@npm:3.21.0"