diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index dab22c9..27f4d2a 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -54,6 +54,9 @@ jobs: run: | go test -v ./... + - name: Generate (and validate) translations + run: make translate + - name: Build release run: make publish env: diff --git a/README.md b/README.md index ccf41f8..576f473 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,11 @@ expiryChoices: [300, ...] # root of the application (i.e. an app.png would be served at # https://ots.example.com/app.png). overlayFSPath: /path/to/ots-customization + +# Switch to formal translations for languages having those defined. +# Languages not having a formal version will still display the normal +# translations in the respective language. +useFormalLanguage: false ``` To override the styling of the application have a look at the [`src/style.scss`](./src/style.scss) file how the theme of the application is built and present the compiled `app.css` in the `overlayFSPath`. @@ -135,3 +140,27 @@ If you want to help translating the application to your own language please see Of course you also could open a pull-request to add the new translations to the `i18n.yaml` file. Same goes with when you're finding translation errors: Just open an issue and let me know! + +The format for the `i18n.yaml` is as follows: +```yaml +reference: # Reference strings (English) + deeplLanguage: en # Source language for DeepL automated translations + languageKey: en # Browser language to use this translation for + translations: {} # Map of translation keys to their translations + +translations: # Translations into other languages + de: # Identifier for the language, used as `languageKey` + deeplLanguage: de # Target language for DeepL automated translations + translations: {} # Informal / base translations for the language. + # Missing keys will be loaded from the `reference` + # and therefore get displayed in English. Missing + # keys can be generated through DeepL through the + # translation tool included in `ci/translate` but + # will have low quality as partial sentences or + # even only words lack the context for the + # translation + formalTranslations: {} # Formal translations for the language (these will + # be merged over the `translations` for this language + # so you don't have to copy keys being equal in formal + # and informal translation.) +``` diff --git a/ci/translate/go.mod b/ci/translate/go.mod index be38e4b..066a227 100644 --- a/ci/translate/go.mod +++ b/ci/translate/go.mod @@ -3,15 +3,28 @@ module translate go 1.20 require ( + github.com/Luzifer/go_helpers/v2 v2.20.0 github.com/Luzifer/rconfig/v2 v2.4.0 + github.com/Masterminds/sprig/v3 v3.2.3 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.11 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/spf13/cast v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + golang.org/x/crypto v0.3.0 // indirect + golang.org/x/sys v0.9.0 // indirect gopkg.in/validator.v2 v2.0.0-20210331031555-b37d688a7fb0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/ci/translate/go.sum b/ci/translate/go.sum index b2b2d68..29c8a70 100644 --- a/ci/translate/go.sum +++ b/ci/translate/go.sum @@ -1,25 +1,97 @@ +github.com/Luzifer/go_helpers/v2 v2.20.0 h1:OyCUs7TFGwfJpGqD21KEKKOXy92jetw2l7dlmG7HZnA= +github.com/Luzifer/go_helpers/v2 v2.20.0/go.mod h1:KPGjImwm51SmOTZMd9XUsT241gHYJuEyLrS/omQ4/Dw= github.com/Luzifer/rconfig/v2 v2.4.0 h1:MAdymTlExAZ8mx5VG8xOFAtFQSpWBipKYQHPOmYTn9o= github.com/Luzifer/rconfig/v2 v2.4.0/go.mod h1:hWF3ZVSusbYlg5bEvCwalEyUSY+0JPJWUiIu7rBmav8= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/validator.v2 v2.0.0-20210331031555-b37d688a7fb0 h1:EFLtLCwd8tGN+r/ePz3cvRtdsfYNhDEdt/vp6qsT+0A= gopkg.in/validator.v2 v2.0.0-20210331031555-b37d688a7fb0/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ci/translate/translate.go b/ci/translate/translate.go index 9616b25..9cfb2b8 100644 --- a/ci/translate/translate.go +++ b/ci/translate/translate.go @@ -10,6 +10,7 @@ import ( "text/template" "time" + "github.com/Masterminds/sprig/v3" "github.com/pkg/errors" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" @@ -17,17 +18,7 @@ import ( "github.com/Luzifer/rconfig/v2" ) -const ( - deeplRequestTimeout = 10 * time.Second - jsTemplate = `// Auto-Generated, do not edit! - -export default { -{{- range $lang, $translation := .Translations }} - '{{ $lang }}': JSON.parse('{{ .Translations.ToJSON }}'), -{{- end }} -} -` -) +const deeplRequestTimeout = 10 * time.Second type ( translation map[string]any @@ -36,19 +27,23 @@ type ( Translations map[string]*translationMapping `yaml:"translations"` } translationMapping struct { - DeeplLanguage string `yaml:"deeplLanguage,omitempty"` - LanguageKey string `yaml:"languageKey,omitempty"` - Translations translation `yaml:"translations"` + DeeplLanguage string `yaml:"deeplLanguage,omitempty"` + LanguageKey string `yaml:"languageKey,omitempty"` + Translations translation `yaml:"translations"` + FormalTranslations translation `yaml:"formalTranslations,omitempty"` } ) var ( cfg = struct { + AutoTranslate bool `flag:"auto-translate" default:"false" description:"Enable auto-translation through DeepL"` DeeplAPIEndpoint string `flag:"deepl-api-endpoint" default:"https://api-free.deepl.com/v2/translate" description:"DeepL API endpoint to request translations from"` DeeplAPIKey string `flag:"deepl-api-key" default:"" description:"API key for the DeepL API"` OutputFile string `flag:"output-file,o" default:"../../src/langs/langs.js" description:"Where to put rendered translations"` + Template string `flag:"template" default:"../../src/langs/langs.tpl.js" description:"Template to load for translation JS file"` TranslationFile string `flag:"translation-file,t" default:"../../i18n.yaml" description:"File to use for translations"` LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"` + Verify bool `flag:"verify" default:"true" description:"Run verification against translation file"` VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"` }{} @@ -88,10 +83,20 @@ func main() { logrus.WithError(err).Fatal("loading translation file") } - logrus.Info("auto-translating new strings...") + if cfg.AutoTranslate { + logrus.Info("auto-translating new strings...") + + if err = autoTranslate(&tf); err != nil { + logrus.WithError(err).Fatal("adding missing translations") + } + } + + if cfg.Verify { + logrus.Info("verify translation file...") - if err = autoTranslate(&tf); err != nil { - logrus.WithError(err).Fatal("adding missing translations") + if err = verify(tf); err != nil { + logrus.WithError(err).Fatal("verifying translations") + } } logrus.Info("saving translation file...") @@ -234,11 +239,19 @@ func loadTranslationFile() (translationFile, error) { } defer f.Close() - return tf, errors.Wrap(yaml.NewDecoder(f).Decode(&tf), "decoding translation file") + decoder := yaml.NewDecoder(f) + decoder.KnownFields(true) + + return tf, errors.Wrap(decoder.Decode(&tf), "decoding translation file") } func renderJSFile(tf translationFile) error { - tpl, err := template.New("js").Parse(jsTemplate) + jsTemplate, err := os.ReadFile(cfg.Template) + if err != nil { + return errors.Wrap(err, "reading template file") + } + + tpl, err := template.New("js").Funcs(sprig.FuncMap()).Parse(string(jsTemplate)) if err != nil { return errors.Wrap(err, "parsing template") } diff --git a/ci/translate/verify.go b/ci/translate/verify.go new file mode 100644 index 0000000..a4fd6d7 --- /dev/null +++ b/ci/translate/verify.go @@ -0,0 +1,94 @@ +package main + +import ( + "reflect" + "regexp" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/Luzifer/go_helpers/v2/str" +) + +var langKeyFormat = regexp.MustCompile(`^[a-z]{2}(-[A-Z]{2})?$`) + +func verify(tf translationFile) error { + var ( + err error + keys []string + keyType = map[string]reflect.Type{} + ) + + for k, v := range tf.Reference.Translations { + keys = append(keys, k) + keyType[k] = reflect.TypeOf(v) + } + + if !langKeyFormat.MatchString(tf.Reference.LanguageKey) { + return errors.New("reference contains invalid languageKey") + } + + if len(keys) == 0 { + return errors.New("reference does not contain translations") + } + + logrus.Infof("found %d translation keys in reference", len(keys)) + + if tf.Reference.FormalTranslations != nil { + if verifyTranslationKeys(logrus.NewEntry(logrus.StandardLogger()), tf.Reference.FormalTranslations, keys, keyType, false); err != nil { + return errors.New("reference contains error in formalTranslations") + } + } + + var hadErrors bool + for lk, tm := range tf.Translations { + logger := logrus.WithField("lang", lk) + logger.Info("validating language...") + + if !langKeyFormat.MatchString(lk) { + hadErrors = true + logger.Error("language key is invalid") + } + + if tm.DeeplLanguage == "" { + logger.Info("no deeplLanguage is set") + } + + hadErrors = hadErrors || verifyTranslationKeys(logger, tm.Translations, keys, keyType, true) + hadErrors = hadErrors || verifyTranslationKeys(logger, tm.FormalTranslations, keys, keyType, false) + } + + if hadErrors { + return errors.New("translation file has errors") + } + return nil +} + +func verifyTranslationKeys(logger *logrus.Entry, t translation, keys []string, keyType map[string]reflect.Type, warnMissing bool) (hadErrors bool) { + var seenKeys []string + + for k, v := range t { + keyLogger := logger.WithField("translation_key", k) + if !str.StringInSlice(k, keys) { + // Contains extra key, is error + hadErrors = true + keyLogger.Error("extra key found") + continue // No further checks for that key + } + + seenKeys = append(seenKeys, k) + if kt := reflect.TypeOf(v); keyType[k] != kt { + // Type mismatches (i.e. string vs []string) + hadErrors = true + keyLogger.Errorf("key has invalid type %s != %s", kt, keyType[k]) + } + } + + for _, k := range keys { + if warnMissing && !str.StringInSlice(k, seenKeys) { + logger.WithField("translation_key", k).Warn("missing translation") + } + } + + return hadErrors +} diff --git a/customize.go b/customize.go index 9a52189..9a99e48 100644 --- a/customize.go +++ b/customize.go @@ -21,6 +21,7 @@ type ( DisableThemeSwitcher bool `json:"disableThemeSwitcher,omitempty" yaml:"disableThemeSwitcher"` ExpiryChoices []int64 `json:"expiryChoices,omitempty" yaml:"expiryChoices"` OverlayFSPath string `json:"-" yaml:"overlayFSPath"` + UseFormalLanguage bool `json:"-" yaml:"useFormalLanguage"` } ) diff --git a/frontend/index.html b/frontend/index.html index f6cea32..8c887b8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -51,6 +51,7 @@ const maxSecretExpire = {{ .MaxSecretExpiry }} const version = "{{ .Version }}" window.OTSCustomize = JSON.parse('{{ .Customize.ToJSON }}') + window.useFormalLanguage = {{ .Customize.UseFormalLanguage | mustToJson }}
diff --git a/go.mod b/go.mod index 313c432..463da57 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/Luzifer/go_helpers/v2 v2.20.0 github.com/Luzifer/rconfig/v2 v2.4.0 + github.com/Masterminds/sprig/v3 v3.2.3 github.com/gofrs/uuid v4.4.0+incompatible github.com/gorilla/mux v1.8.0 github.com/pkg/errors v0.9.1 @@ -14,9 +15,19 @@ require ( ) require ( + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.11 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/spf13/cast v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.3.0 // indirect golang.org/x/sys v0.9.0 // indirect gopkg.in/validator.v2 v2.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6181072..06433c8 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,12 @@ github.com/Luzifer/go_helpers/v2 v2.20.0 h1:OyCUs7TFGwfJpGqD21KEKKOXy92jetw2l7dl github.com/Luzifer/go_helpers/v2 v2.20.0/go.mod h1:KPGjImwm51SmOTZMd9XUsT241gHYJuEyLrS/omQ4/Dw= github.com/Luzifer/rconfig/v2 v2.4.0 h1:MAdymTlExAZ8mx5VG8xOFAtFQSpWBipKYQHPOmYTn9o= github.com/Luzifer/rconfig/v2 v2.4.0/go.mod h1:hWF3ZVSusbYlg5bEvCwalEyUSY+0JPJWUiIu7rBmav8= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -15,10 +21,20 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4 github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= @@ -28,23 +44,60 @@ 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/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY= gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/i18n.yaml b/i18n.yaml index 64065f4..c4b49f9 100644 --- a/i18n.yaml +++ b/i18n.yaml @@ -98,6 +98,22 @@ translations: title-reading-secret: Secret auslesen… title-secret-create-disabled: Erstellen von Secrets deaktiviert… title-secret-created: Secret erstellt! + formalTranslations: + alert-secret-not-found: Dieses Secret existiert nicht. - Falls Sie diesen Link noch nicht selbst geöffnet haben, könnte der Inhalt kompromittiert sein, da jemand anderes den Link geöffnet haben könnte. + btn-reveal-secret: Secret anzeigen + items-explanation: + - Sie geben ein Secret auf dieser Seite ein + - Ihr Browser verschlüsselt das Secret mit einem generierten Passwort + - Nur das verschlüsselte Secret wird an den Server geschickt (das Passwort oder das Secret im Klartext werden niemals übertragen!) + - Der Server speichert das verschlüsselte Secret für eine Weile + - Sie geben die angezeigte URL, welche die ID und das Passwort des Secrets enthält, an den Empfänger + - 'Der Empfänger kann das Secret einmalig abrufen: Funktioniert das nicht, könnte jemand anderes es abgerufen haben!' + - Wenn das verschlüsselte Secret das erste Mal abgerufen wurde, wird es automatisch vom Server gelöscht + text-burn-hint: Bitte rufen Sie die URL nicht selbst auf, da das Secret dadurch zerstört würde. + text-hint-burned: Achtung: Sie können das Secret nur einmal ansehen! Sobald Sie die Seite neu laden, kann das Secret nicht erneut abgerufen werden, also besser direkt kopieren und sicher abspeichern… + text-pre-reveal-hint: Klicken Sie auf diesen Button um das Secret anzuzeigen, bedenken Sie aber, dass das Secret nur einmal angezeigt und dabei gelöscht wird. + text-pre-url: 'Das Secret wurde angelegt und unter folgender URL gespeichert:' + title-new-secret: Ein neues Secret erstellen es: deeplLanguage: es translations: diff --git a/src/langs/langs.js b/src/langs/langs.js index 276f560..31f6376 100644 --- a/src/langs/langs.js +++ b/src/langs/langs.js @@ -1,18 +1,39 @@ // Auto-Generated, do not edit! +const switchFormal = (formal, informal) => { + return window.useFormalLanguage ? formal : informal +} + export default { 'ca': JSON.parse('{"alert-secret-not-found":"Aquest no és el secret que busques\u0026hellip; - Si esperaves que el secret estiguera ací, és possible que s\'haja vist compromés, ja que una altra persona podria haver obert l\'enllaç en comptes de tu.","alert-something-went-wrong":"Alguna cosa ha eixit malament. Ens sap molt greu\u0026hellip;","btn-create-secret":"Crea el secret!","btn-new-secret":"Nou secret","btn-reveal-secret":"Mostra\'m el secret!","btn-show-explanation":"Com funciona?","items-explanation":["Introduïx un secret en el formulari que hi ha en aquesta pàgina","El teu navegador xifra el secret utilitzant una contrasenya generada","Únicament s\'envia al servidor el secret xifrat (mai s\'envien ni el secret sense xifrar ni la contrasenya!)","El servidor emmagatzema el secret xifrat durant un temps limitat","Envia al destinatari l\'enllaç mostrat, que conté l\'identificador del secret i la contrasenya de desxifrat","El destinatari pot veure el secret una sola vegada: si no pot, el secret podria haver sigut vist per una altra persona!","Quan s\'ha obtingut per primera i única vegada el secret xifrat, s\'elimina del servidor"],"label-secret-data":"Informació secreta:","text-burn-hint":"Per favor, recorda no accedir a aquest enllaç tu mateix, ja que això destruiria el secret. Només has de passar-li\'l a una altra persona!","text-hint-burned":"\u003cstrong\u003eAtenció:\u003c/strong\u003e Només veuràs això una vegada. Quan recarregues la pàgina, el secret desapareixerà, així que copia\'l ja\u0026hellip;","text-powered-by":"Funciona amb","text-pre-reveal-hint":"Per a mostrar el secret prem aquest botó, però tingues en compte que en fer-ho es destruirà. Només pots veure\'l una vegada!","text-pre-url":"El teu secret ha sigut creat i emmagatzemat en el següent enllaç:","title-explanation":"Així és com funciona\u0026hellip;","title-new-secret":"Crea un nou secret","title-reading-secret":"Obtenint el teu secret\u0026hellip;","title-secret-created":"Secret creat!"}'), - 'de': JSON.parse('{"alert-secret-not-found":"Das ist nicht das Secret, was du suchst\u0026hellip; - Falls du diesen Link noch nicht selbst geöffnet hast, könnte das Secret kompromittiert sein, da jemand anderes den Link geöffnet haben könnte.","alert-something-went-wrong":"Irgendwas ging schief. Entschuldigung\u0026hellip;","btn-create-secret":"Secret erstellen!","btn-new-secret":"Neues Secret","btn-reveal-secret":"Zeig mir das Secret!","btn-show-explanation":"Wie funktioniert das?","expire-default":"Server-Standard","expire-n-days":"{n} Tag | {n} Tage","expire-n-hours":"{n} Stunde | {n} Stunden","expire-n-minutes":"{n} Minute | {n} Minuten","expire-n-seconds":"{n} Sekunde | {n} Sekunden","items-explanation":["Du gibst ein Secret auf dieser Seite ein","Dein Browser verschlüsselt das Secret mit einem generierten Passwort","Nur das verschlüsselte Secret wird an den Server geschickt (das Passwort oder das Secret im Klartext werden niemals übertragen!)","Der Server speichert das verschlüsselte Secret für eine Weile","Du gibst die angezeigte URL, welche die ID und das Passwort des Secrets enthält, an den Empfänger","Der Empfänger kann das Secret einmalig abrufen: Funktioniert das nicht, könnte jemand anderes es abgerufen haben!","Wenn das verschlüsselte Secret das erste Mal abgerufen wurde, wird es automatisch vom Server gelöscht"],"label-expiry":"Ablauf in:","label-secret-data":"Inhalt des Secrets:","text-burn-hint":"Bitte rufe die URL nicht selbst auf, da das Secret dadurch zerstört würde. Gib sie einfach weiter!","text-burn-time":"Wenn es vorher nicht eingesehen wurde, wird dieses Secret automatisch gelöscht:","text-hint-burned":"\u003cstrong\u003eAchtung:\u003c/strong\u003e Du kannst das nur einmal ansehen! Sobald du die Seite neu lädst, ist das Secret verschwunden, also besser direkt kopieren und sicher abspeichern\u0026hellip;","text-powered-by":"Läuft mit","text-pre-reveal-hint":"Um das Secret anzuzeigen klicke diesen Button aber denk dran, dass das Secret nur einmal angezeigt und dabei gelöscht wird.","text-pre-url":"Dein Secret wurde angelegt und unter folgender URL gespeichert:","text-secret-create-disabled":"Auf dieser Instanz wurde das Erstellen neuer Secrets deaktiviert.","title-explanation":"So funktioniert es\u0026hellip;","title-new-secret":"Erstelle ein neues Secret","title-reading-secret":"Secret auslesen\u0026hellip;","title-secret-create-disabled":"Erstellen von Secrets deaktiviert…","title-secret-created":"Secret erstellt!"}'), + + 'de': switchFormal( + JSON.parse('{"alert-secret-not-found":"Dieses Secret existiert nicht. - Falls Sie diesen Link noch nicht selbst geöffnet haben, könnte der Inhalt kompromittiert sein, da jemand anderes den Link geöffnet haben könnte.","alert-something-went-wrong":"Irgendwas ging schief. Entschuldigung\u0026hellip;","btn-create-secret":"Secret erstellen!","btn-new-secret":"Neues Secret","btn-reveal-secret":"Secret anzeigen","btn-show-explanation":"Wie funktioniert das?","expire-default":"Server-Standard","expire-n-days":"{n} Tag | {n} Tage","expire-n-hours":"{n} Stunde | {n} Stunden","expire-n-minutes":"{n} Minute | {n} Minuten","expire-n-seconds":"{n} Sekunde | {n} Sekunden","items-explanation":["Sie geben ein Secret auf dieser Seite ein","Ihr Browser verschlüsselt das Secret mit einem generierten Passwort","Nur das verschlüsselte Secret wird an den Server geschickt (das Passwort oder das Secret im Klartext werden niemals übertragen!)","Der Server speichert das verschlüsselte Secret für eine Weile","Sie geben die angezeigte URL, welche die ID und das Passwort des Secrets enthält, an den Empfänger","Der Empfänger kann das Secret einmalig abrufen: Funktioniert das nicht, könnte jemand anderes es abgerufen haben!","Wenn das verschlüsselte Secret das erste Mal abgerufen wurde, wird es automatisch vom Server gelöscht"],"label-expiry":"Ablauf in:","label-secret-data":"Inhalt des Secrets:","text-burn-hint":"Bitte rufen Sie die URL nicht selbst auf, da das Secret dadurch zerstört würde.","text-burn-time":"Wenn es vorher nicht eingesehen wurde, wird dieses Secret automatisch gelöscht:","text-hint-burned":"\u003cstrong\u003eAchtung:\u003c/strong\u003e Sie können das Secret nur einmal ansehen! Sobald Sie die Seite neu laden, kann das Secret nicht erneut abgerufen werden, also besser direkt kopieren und sicher abspeichern\u0026hellip;","text-powered-by":"Läuft mit","text-pre-reveal-hint":"Klicken Sie auf diesen Button um das Secret anzuzeigen, bedenken Sie aber, dass das Secret nur einmal angezeigt und dabei gelöscht wird.","text-pre-url":"Das Secret wurde angelegt und unter folgender URL gespeichert:","text-secret-create-disabled":"Auf dieser Instanz wurde das Erstellen neuer Secrets deaktiviert.","title-explanation":"So funktioniert es\u0026hellip;","title-new-secret":"Ein neues Secret erstellen","title-reading-secret":"Secret auslesen\u0026hellip;","title-secret-create-disabled":"Erstellen von Secrets deaktiviert…","title-secret-created":"Secret erstellt!"}'), + JSON.parse('{"alert-secret-not-found":"Das ist nicht das Secret, was du suchst\u0026hellip; - Falls du diesen Link noch nicht selbst geöffnet hast, könnte das Secret kompromittiert sein, da jemand anderes den Link geöffnet haben könnte.","alert-something-went-wrong":"Irgendwas ging schief. Entschuldigung\u0026hellip;","btn-create-secret":"Secret erstellen!","btn-new-secret":"Neues Secret","btn-reveal-secret":"Zeig mir das Secret!","btn-show-explanation":"Wie funktioniert das?","expire-default":"Server-Standard","expire-n-days":"{n} Tag | {n} Tage","expire-n-hours":"{n} Stunde | {n} Stunden","expire-n-minutes":"{n} Minute | {n} Minuten","expire-n-seconds":"{n} Sekunde | {n} Sekunden","items-explanation":["Du gibst ein Secret auf dieser Seite ein","Dein Browser verschlüsselt das Secret mit einem generierten Passwort","Nur das verschlüsselte Secret wird an den Server geschickt (das Passwort oder das Secret im Klartext werden niemals übertragen!)","Der Server speichert das verschlüsselte Secret für eine Weile","Du gibst die angezeigte URL, welche die ID und das Passwort des Secrets enthält, an den Empfänger","Der Empfänger kann das Secret einmalig abrufen: Funktioniert das nicht, könnte jemand anderes es abgerufen haben!","Wenn das verschlüsselte Secret das erste Mal abgerufen wurde, wird es automatisch vom Server gelöscht"],"label-expiry":"Ablauf in:","label-secret-data":"Inhalt des Secrets:","text-burn-hint":"Bitte rufe die URL nicht selbst auf, da das Secret dadurch zerstört würde. Gib sie einfach weiter!","text-burn-time":"Wenn es vorher nicht eingesehen wurde, wird dieses Secret automatisch gelöscht:","text-hint-burned":"\u003cstrong\u003eAchtung:\u003c/strong\u003e Du kannst das nur einmal ansehen! Sobald du die Seite neu lädst, ist das Secret verschwunden, also besser direkt kopieren und sicher abspeichern\u0026hellip;","text-powered-by":"Läuft mit","text-pre-reveal-hint":"Um das Secret anzuzeigen klicke diesen Button aber denk dran, dass das Secret nur einmal angezeigt und dabei gelöscht wird.","text-pre-url":"Dein Secret wurde angelegt und unter folgender URL gespeichert:","text-secret-create-disabled":"Auf dieser Instanz wurde das Erstellen neuer Secrets deaktiviert.","title-explanation":"So funktioniert es\u0026hellip;","title-new-secret":"Erstelle ein neues Secret","title-reading-secret":"Secret auslesen\u0026hellip;","title-secret-create-disabled":"Erstellen von Secrets deaktiviert…","title-secret-created":"Secret erstellt!"}'), + ), + 'en': JSON.parse('{"alert-secret-not-found":"This is not the secret you are looking for\u0026hellip; - If you expected the secret to be here it might be compromised as someone else might have opened the link already.","alert-something-went-wrong":"Something went wrong. I\'m very sorry about this\u0026hellip;","btn-create-secret":"Create the secret!","btn-new-secret":"New Secret","btn-reveal-secret":"Show me the secret!","btn-show-explanation":"How does this work?","expire-default":"Default Expiry","expire-n-days":"{n} day | {n} days","expire-n-hours":"{n} hour | {n} hours","expire-n-minutes":"{n} minute | {n} minutes","expire-n-seconds":"{n} second | {n} seconds","items-explanation":["You enter a secret into the field on this page","Your browser encrypts the secret using a generated password","Only the encrypted secret is sent to the server (neither the plain secret nor the password are ever sent!)","The server stores the encrypted secret for a certain time","You pass the displayed URL containing the ID and the decryption password to the recipient","The recipient can view the secret exactly once: If they can\'t, the secret might have been viewed by someone else!","After the encrypted secret has been retrieved once, it is deleted from the server"],"label-expiry":"Expire in:","label-secret-data":"Secret data:","text-burn-hint":"Please remember not to go to this URL yourself as that would destroy the secret. Just pass it to someone else!","text-burn-time":"If not viewed before, this secret will automatically be deleted:","text-hint-burned":"\u003cstrong\u003eAttention:\u003c/strong\u003e You\'re only seeing this once. As soon as you reload the page the secret will be gone so maybe copy it now\u0026hellip;","text-powered-by":"Powered by","text-pre-reveal-hint":"To reveal the secret click this button but be aware doing so will destroy the secret. You can only view it once!","text-pre-url":"Your secret was created and stored using this URL:","text-secret-create-disabled":"The creation of new secrets is disabled in this instance.","title-explanation":"This is how it works\u0026hellip;","title-new-secret":"Create a new secret","title-reading-secret":"Reading your secret\u0026hellip;","title-secret-create-disabled":"Secret creation disabled…","title-secret-created":"Secret created!"}'), + 'es': JSON.parse('{"alert-secret-not-found":"Este no es el secreto que buscas\u0026hellip; - Si esperabas que el secreto estuviera aquí, es posible que se haya visto comprometido, ya que otra persona podría haber abierto el enlace en tu lugar.","alert-something-went-wrong":"Algo ha salido mal. Lo sentimos mucho\u0026hellip;","btn-create-secret":"¡Crea el secreto!","btn-new-secret":"Nuevo secreto","btn-reveal-secret":"¡Muéstrame el secreto!","btn-show-explanation":"¿Cómo funciona?","expire-n-days":"{n} día | {n} días","expire-n-hours":"{n} hora | {n} horas","expire-n-minutes":"{n} minuto | {n} minutos","expire-n-seconds":"{n} segundo | {n} segundos","items-explanation":["Introduce un secreto en el formulario que hay en esta página","Tu navegador cifra el secreto utilizando una contraseña generada","Únicamente se envía al servidor el secreto cifrado (¡nunca se envían ni el secreto sin cifrar ni la contraseña!)","El servidor almacena el secreto cifrado durante un tiempo limitado","Envía al destinatario el enlace mostrado, que contiene el identificador del secreto y la contraseña de descifrado","El destinatario puede ver el secreto una sola vez: si no puede, ¡el secreto podría haber sido visto por otra persona!","Cuando se ha obtenido por primera y única vez el secreto cifrado, se elimina del servidor"],"label-secret-data":"Información secreta:","text-burn-hint":"Por favor, recuerda no acceder a este enlace tú mismo, ya que esto destruiría el secreto. ¡Solo tienes que pasárselo a otra persona!","text-hint-burned":"\u003cstrong\u003eAtención:\u003c/strong\u003e Solo verás esto una vez. En cuanto recargues la página, el secreto desaparecerá, así que cópialo ya\u0026hellip;","text-powered-by":"Funciona con","text-pre-reveal-hint":"Para mostrar el secreto pulsa este botón, pero ten en cuenta que al hacerlo se destruirá. ¡Solo puedes verlo una vez!","text-pre-url":"Tu secreto ha sido creado y almacenado en el siguiente enlace:","text-secret-create-disabled":"En este caso, la creación de nuevos secretos está desactivada.","title-explanation":"Así es como funciona\u0026hellip;","title-new-secret":"Crea un nuevo secreto","title-reading-secret":"Obteniendo tu secreto\u0026hellip;","title-secret-create-disabled":"Creación secreta desactivada...","title-secret-created":"¡Secreto creado!"}'), + 'fr': JSON.parse('{"alert-secret-not-found":"Ce secret n\'est pas celui que vous cherchez\u0026hellip; - Si vous comptiez trouvez ce secret ici, il a pu être compromis car quelqu\'un a probablement déjà ouvert le lien.","alert-something-went-wrong":"Un problème est survenu. Nous en sommes désolés\u0026hellip;","btn-create-secret":"Créer le secret!","btn-new-secret":"Nouveau secret","btn-reveal-secret":"Voir le secret!","btn-show-explanation":"Comment ça fonctionne?","expire-n-days":"{n} jour | {n} jours","expire-n-hours":"{n} heure | {n} heures","expire-n-minutes":"{n} minute | {n} minutes","expire-n-seconds":"{n} seconde | {n} secondes","items-explanation":["Vous saisissez le secret dans un champ sur cette page","Votre navigateur chiffre le secret en utilisant un mot de passe généré","Seul le secret chiffré est envoyé au serveur (ni le secret en clair, ni le mot de passe ne sont envoyés!)","Le serveur stocke le secret chiffré pendant un certain temps","Vous fournissez l\'URL affichée contenant l\'identifiant et le mot de passe de déchiffrage au destinataire","Le destintaire ne peut voir le secret qu\'une fois: si cela ne fonctionne pas, c\'est que le secret a été consulté par quelqu\'un d\'autre!","Dès que le secret chiffré a été récupéré, il est supprimé du serveur"],"label-secret-data":"Données secrètes:","text-burn-hint":"Attention de ne pas ouvrir cette URL vous-même, cela détruirait le secret. Fournissez-la à quelqu\'un d\'autre!","text-hint-burned":"\u003cstrong\u003eAttention:\u003c/strong\u003e Vous ne pouvez consulter ce contenu qu\'une fois. Le secret sera détruit dès que vous rechargez la page, donc copiez le maintenant\u0026hellip;","text-powered-by":"Propulsé par","text-pre-reveal-hint":"Pour afficher le secret, cliquez sur ce bouton, mais soyez conscient que cela le détruira. Vous ne pouvez l\'afficher qu\'une fois!","text-pre-url":"Votre secret a été créé et stocké à cette URL:","text-secret-create-disabled":"La création de nouveaux secrets est désactivée dans ce cas.","title-explanation":"Voici comment ça fonctionne\u0026hellip;","title-new-secret":"Créer un nouveau secret","title-reading-secret":"Lecture du secret\u0026hellip;","title-secret-create-disabled":"Création secrète désactivée...","title-secret-created":"Secret créé!"}'), + 'lv': JSON.parse('{"alert-secret-not-found":"\u003cstrong\u003eZiņa nav atrasta!\u003c/strong\u003e\u0026hellip; - Ja ievadītā saite ir pareiza, tad ir beidzies ziņas glabāšanas laiks, vai arī tā jau vienreiz ir atvērta.","alert-something-went-wrong":"Neparedzēta sistēmas kļūda. Atvainojiet par sagādātajām neērtībām\u0026hellip;","btn-create-secret":"Šifrēt ziņu!","btn-new-secret":"Jauna ziņa","btn-reveal-secret":"Atvērt ziņu!","btn-show-explanation":"Kā tas strādā?","expire-n-days":"{n} diena | {n} dienas","expire-n-hours":"{n} stunda | {n} stundas","expire-n-minutes":"{n} minūte | {n} minūtes","expire-n-seconds":"{n} sekundes | {n} sekundes","items-explanation":["Tu ievadi ziņu ievades laukā","Pārlūks nošifrē ziņu ar uzģenerētu paroli","Tikai šifrētā ziņa tiek nosūtīta serverim (nešifrētā ziņa un parole sūtīta netiek!)","Serveris noteiktu laiku glabā šifrēto ziņu","Tu nodod URL ar ziņas ID un atšifrēšanas paroli saņēmējam","Saņēmējs var atvērt ziņu tikai vienreiz: ja tas neizdodas, iespējams, ziņu jau atvēris kāds cits!","Kad ziņa tiek atvērta pirmo reizi, tā no servera tiek dzēsta"],"label-secret-data":"Ziņa:","text-burn-hint":"Lūdzu atceries neatvērt saiti pats, jo tad ziņa tiks dzēsta. Nodod saiti ziņas saņēmējam!","text-hint-burned":"\u003cstrong\u003eUzmanību:\u003c/strong\u003e Ziņa tiek parādīta tikai vienu reizi. Līdzko lapa tiks pārlādēta, ziņa būs neatgriezeniski zaudēta, tāpēc nepieciešamības gadījumā nokopē to tagad\u0026hellip;","text-powered-by":"Darbina","text-pre-reveal-hint":"Lai parādītu ziņu nospied šo pogu, bet rēķinies ar to, ka pēc apskates ziņa vairs nebūs pieejama. To var atvērt tikai vienreiz!","text-pre-url":"Ziņa ir nošifrēta un ir atverama šajā adresē:","text-secret-create-disabled":"Šajā gadījumā jaunu noslēpumu izveide ir atspējota.","title-explanation":"Tā tas strādā\u0026hellip;","title-new-secret":"Šifrēt ziņu","title-reading-secret":"Atver ziņu\u0026hellip;","title-secret-create-disabled":"Slepena izveide atspējota...","title-secret-created":"Ziņa nošifrēta!"}'), + 'nl': JSON.parse('{"alert-secret-not-found":"De gegevens die je zocht bestaan niet (meer)\u0026hellip; - Als je hier informatie verwachtte dan is de link mogelijk al door iemand anders bekeken!","alert-something-went-wrong":"Er ging iets verkeerd, sorry\u0026hellip;","btn-create-secret":"Nieuwe vertrouwelijke info aanmaken!","btn-new-secret":"Nieuw","btn-reveal-secret":"Toon mij de vertrouwelijke info!","btn-show-explanation":"Hoe werkt dit?","expire-n-days":"{n} dag | {n} dagen","expire-n-hours":"{n} uur | {n} uur","expire-n-minutes":"{n} minute | {n} minuten","expire-n-seconds":"{n} seconde | {n} seconden","items-explanation":["Je vult vertrouwelijke informatie in op deze pagina.","Je browser versleutelt de ingevulde tekst via een automatisch gegenereerd wachtwoord.","Alleen de versleutelde data wordt naar de server gestuurd. (De leesbare versie of het wachtwoord worden nooit verstuurd!)","De server slaat de versleutelde data gedurende een beperkte periode op.","Je geeft de URL met identificatie en het gegenereerde wachtwoord aan de ontvanger.","De ontvanger kan de vertrouwelijke informatie exact eenmaal bekijken: indien het niet lukt heeft mogelijk iemand anders de info gezien!","De versleutelde data wordt van de server gewist van zodra de ontvanger het bekeken heeft."],"label-secret-data":"Vertrouwelijke info:","text-burn-hint":"Bezoek de URL niet zelf: je kan deze slechts eenmaal gebruiken. Geef de URL aan de ontvanger.","text-hint-burned":"\u003cstrong\u003eOpgelet:\u003c/strong\u003e Je ziet deze informatie alleen nu. Je kan het niet meer opnieuw opvragen als je de pagina verlaat.","text-powered-by":"Mogelijk gemaakt door","text-pre-reveal-hint":"Gebruik deze knop om de vertrouwelijke info op te halen. Let op: Je kan dit slechts eenmaal doen!","text-pre-url":"Je vertrouwelijke informatie kan opgevraagd worden via deze URL:","text-secret-create-disabled":"Het aanmaken van nieuwe geheimen is in dit geval uitgeschakeld.","title-explanation":"Dit is hoe het werkt\u0026hellip;","title-new-secret":"Nieuwe vertrouwelijke info opslaan","title-reading-secret":"Vertrouwelijke info lezen\u0026hellip;","title-secret-create-disabled":"Geheime creatie uitgeschakeld...","title-secret-created":"Vertrouwelijke info opgeslaan!"}'), + 'pt-BR': JSON.parse('{"alert-secret-not-found":"Esta não é o segredo que você está procurando… - Se você esperava que o segredo estaria aqui, ele pode ter sido comprometido por alguém que já acessou o link.","alert-something-went-wrong":"Desculpe, algo deu errado…","btn-create-secret":"Criar segredo!","btn-new-secret":"Novo segredo","btn-reveal-secret":"Mostrar o segredo!","btn-show-explanation":"Como funciona?","expire-n-days":"{n} dia | {n} dias","expire-n-hours":"{n} hora | {n} horas","expire-n-minutes":"{n} minutos | {n} minutos","expire-n-seconds":"{n} segundos | {n} segundos","items-explanation":["Você insere o segredo no campo de texto desta página","Seu navegador criptografa o segredo usando uma senha gerada","Somente o segredo criptografado é enviado para o servidor (nem o segredo em texto claro, nem a senha é enviada para o servidor!)","O servidor armazena o segredo criptografado por um certo tempo","Você envia a URL mostrada contendo a ID e a senha de descriptografia para o destinatário","O destinatário pode ver o segredo apenas uma vez: se ele não pode ver, o segredo pode ter sido visto por outra pessoa!","Após o segredo ter sido obtido uma vez, o mesmo é deletado do servidor"],"label-secret-data":"Informação secreta:","text-burn-hint":"Importante você lembrar de não acessar esta URL, pois isto irá indisponibilizar o segredo. Apenas encaminhe para outra pessoa!","text-hint-burned":"Atenção: Você está vendo esta informação apenas uma vez. Logo que você recarregar a página o segredo ficará indisponível. É recomendado que você copie a informação agora…","text-powered-by":"Powered by","text-pre-reveal-hint":"Para revelar o segredo clique neste botão, mas lembre-se que esta ação vai destruir o segredo. Você só pode ver uma única vez!","text-pre-url":"Seu segredo foi criado e armazenado na seguinte URL:","text-secret-create-disabled":"A criação de novos segredos é desativada nesse caso.","title-explanation":"É assim como funciona…","title-new-secret":"Criar um novo segredo","title-reading-secret":"Lendo seu segredo…","title-secret-create-disabled":"Criação secreta desativada...","title-secret-created":"Segredo criado!"}'), + 'ru': JSON.parse('{"alert-secret-not-found":"Секрет недоступен\u0026hellip; - Помните, он может быть скомпрометирован. Возможно кто-то другой уже открыл вашу ссылку.","alert-something-went-wrong":"Что-то пошло не так. Приносим свои извинения\u0026hellip;","btn-create-secret":"Создать секрет!","btn-new-secret":"Новый секрет","btn-reveal-secret":"Показать секрет!","btn-show-explanation":"Как это работает?","expire-n-days":"{n} день | {n} дней","expire-n-hours":"{n} час | {n} часов","expire-n-minutes":"{n} минут | {n} минут","expire-n-seconds":"{n} секунда | {n} секунд","items-explanation":["Вы вводите секрет в поле на этой странице.","Ваш браузер шифрует секрет с помощью сгенерированного пароля.","На сервер отправляется только зашифрованный секрет (ни текст секрета, ни пароль никогда не отправляются!)","Сервер хранит зашифрованный секрет в течение определенного времени.","Вы передаете отображаемый URL-адрес, содержащий идентификатор и пароль для расшифровки, получателю.","Получатель может просмотреть секрет ровно один раз: если он не смог, секрет возможно был просмотрен кем-то другим!","После того как зашифрованный секрет был извлечен, он удаляется с сервера."],"label-secret-data":"Секретные данные:","text-burn-hint":"Пожалуйста, не переходите по этому URL для проверки, так как это удалит секрет. Просто скопируйте и передайте его!","text-hint-burned":"\u003cstrong\u003eВнимание:\u003c/strong\u003e Секрет будет показан только один раз. Как только вы перезагрузите страницу, секрет исчезнет, скопируйте его незамедлительно\u0026hellip;","text-powered-by":"Powered by","text-pre-reveal-hint":"Чтобы раскрыть секрет, нажмите эту кнопку, но имейте в виду, что это приведет к уничтожению секрета. Вы можете просмотреть его только один раз!","text-pre-url":"Ваш секрет создан и сохранён, его URL:","text-secret-create-disabled":"Создание новых секретов в этом случае отключено.","title-explanation":"Как это работает\u0026hellip;","title-new-secret":"Создать новый секрет","title-reading-secret":"Читаем ваш секрет\u0026hellip;","title-secret-create-disabled":"Секретное создание отключено...","title-secret-created":"Секрет создан!"}'), + 'sv': JSON.parse('{"alert-secret-not-found":"Hemlighet hittades inte\u0026hellip; - Om du förväntade dig att hemligheten skulle finnas här kan den vara röjd då någon annan kan ha öppnat denna länk tidigare.","alert-something-went-wrong":"Något gick fel. Jag ber om ursäkt för detta!\u0026hellip;","btn-create-secret":"Skapa hemliget!","btn-new-secret":"Ny hemlighet.","btn-reveal-secret":"Visa mig hemligheten!","btn-show-explanation":"Hur fungerar detta?","expire-n-days":"{n} dag | {n} dagar","expire-n-hours":"{n} timme | {n} timmar","expire-n-minutes":"{n} minut | {n} minuter","expire-n-seconds":"{n} sekund | {n} sekunder","items-explanation":["Skriv in en hemlighet i rutan nedan","Din webbläsare krypterar hemligheten med hjälp av ett genererat lösenord","Endast den krypterade hemligheten skickas till servern. (varken lösenordet eller hemligheten i klartext skickas!)","Servern lagrar den krypterade hemligheten för en begränsad tid","Du skickar URL-länken med ID-numret och avkrypteringslösenordet till mottagaren","Mottagaren kan se hemligheten exakt en gång: Om detta misslyckas kan hemligheten redan ha setts av någon annan!","När hemligheten har setts en gång, raderas den från servern"],"label-secret-data":"Hemlig data:","text-burn-hint":"Kom ihåg att inte gå till denna URL själv eftersom detta skulle förbruka hemligheten. Skicka bara vidare den till mottagaren!","text-hint-burned":"\u003cstrong\u003eObservera:\u003c/strong\u003e Du kan endast se denna sida en gång. Så fort du laddar om sidan kommer hemligheten att försvinna så kopiera den nu\u0026hellip;","text-powered-by":"Drivs av","text-pre-reveal-hint":"För att visa hemligheten klicka på denna knapp. Var medveten om att när du gör det kommer hemligheten att förbrukas, du kan endast se den en gång!","text-pre-url":"Din hemlighet har skapats och lagrats med denna URL:","text-secret-create-disabled":"Skapandet av nya hemligheter blockeras i detta fall.","title-explanation":"Såhär fungerar det\u0026hellip;","title-new-secret":"Skapa ny hemlighet","title-reading-secret":"Läs din hemlighet\u0026hellip;","title-secret-create-disabled":"Hemlig skapelse avaktiverad...","title-secret-created":"Hemlighet skapad!"}'), + 'tr': JSON.parse('{"alert-secret-not-found":"Aradığınız sır bu değil… - Sırrın burada olmasını bekliyorsanız, bu link başkası tarafından açılmış ve sırrınız tehlikede olabilir.","alert-something-went-wrong":"Bir şeyler ters gitti. Bunun için çok üzgünüm…","btn-create-secret":"Sır oluştur!","btn-new-secret":"Yeni sır","btn-reveal-secret":"Sırrı göster!","btn-show-explanation":"Nasıl çalışır?","expire-n-days":"{n} gün | {n} gün","expire-n-hours":"{n} saat | {n} saat","expire-n-minutes":"{n} dakika | {n} dakika","expire-n-seconds":"{n} saniye | {n} saniye","items-explanation":["Bu sayfadaki alana sırrınızı giriniz","Internet tarayıcınız oluşturulan şifre yardımı ile sırrınızı enkripte eder","Sadece ektripte edilmiş sır sunucuya gönderilir (ne sır metni nede şifre gönderilmez!)","Sunucu enkripte edilmiş sırrı bünyesinde belli bir süre saklar","Gösterilen linki, id ve deşifre bilgisi ile birlikte alıcıya gönder","Alcı sırrı tam olarak sadece bir kez görebilir: Eğer göremez ise, sır bir başkası tarafından daha önce görülmüş olabilir!","Sır bir kez gösterildikten sonra hemen sunucudan silinir"],"label-secret-data":"Sır bilgisi:","text-burn-hint":"Lütfen linki kendiniz acmayın, bu sırrın silinmesine neden olur. Linki sadece alıcıya gönderin!","text-hint-burned":"Dikkat: Bunu sadece bir kez göreceksiniz. Sayfayı güncellediğinizde yada kapattiğınızda sır kaybolacaktır, belkide şimdi sırrı kopyalamanız akıllıca olacaktır…","text-powered-by":"Tarafından desteklenmektedir","text-pre-reveal-hint":"Sırrı görmek için bu düğmeye tıklayın, ama bunu yaptıktan sonra sırrın silineceğini unutmayın. Bunu sadece bir kez görebilirsin!","text-pre-url":"Sırrınız oluşturuldu ve bu link kullanılarak kaydedildi:","text-secret-create-disabled":"Bu durumda yeni gizli dizilerin oluşturulması devre dışı bırakılır.","title-explanation":"Bu sekilde çalışır…","title-new-secret":"Yeni sır oluştur","title-reading-secret":"Sırrınız okunuyor…","title-secret-create-disabled":"Gizli yaratım devre dışı bırakıldı...","title-secret-created":"Sır oluşturuldu!"}'), + 'uk': JSON.parse('{"alert-secret-not-found":"Це не секрет, який ви шукаєте\u0026hellip; - Якщо ви очікували, що секрет буде тут, він міг бути скомпрометований, оскільки хтось інший міг уже відкрити посилання.","alert-something-went-wrong":"Щось пішло не так. Ми дуже шкодуємо про це\u0026hellip;","btn-create-secret":"Створіть секрет!","btn-new-secret":"Новий секрет","btn-reveal-secret":"Показати мені секрет!","btn-show-explanation":"Як це працює?","expire-default":"Термін дії за замовчуванням","expire-n-days":"{n} день | {n} днів","expire-n-hours":"{n} година | {n} годин","expire-n-minutes":"{n} хвилина | {n} хвилин","expire-n-seconds":"{n} секунда | {n} скунд","items-explanation":["Уведіть секрет у поле на цій сторінці","Ваш браузер шифрує секрет за допомогою згенерованого пароля","На сервер надсилається лише зашифрований секрет (ані простий секрет, ані пароль ніколи не надсилаються!)","Сервер певний час зберігає зашифрований секрет","Ви передаєте одержувачу відображену URL-адресу, яка містить ідентифікатор і пароль для розшифровки","Одержувач може переглянути секрет лише один раз: якщо він не може, секрет міг переглянути хтось інший!","Після того, як зашифрований секрет було отримано один раз, він видаляється з сервера"],"label-expiry":"Термін дії закінчується:","label-secret-data":"Секретні дані:","text-burn-hint":"Пам’ятайте, що не переходьте за цією URL-адресою самостійно, оскільки це знищить секрет. Просто передайте це комусь іншому!","text-burn-time":"Якщо раніше його не переглядати, цей секрет буде автоматично видалено:","text-hint-burned":"\u003cstrong\u003eУвага:\u003c/strong\u003e Ви бачите це лише раз. Щойно ви перезавантажите сторінку, секрет зникне, тому, можливо, скопіюйте його зараз\u0026hellip;","text-powered-by":"Powered by","text-pre-reveal-hint":"Щоб розкрити секрет, натисніть цю кнопку, але майте на увазі, що це знищить секрет. Ви можете переглянути його лише один раз!","text-pre-url":"Ваш секрет було створено та збережено за допомогою цієї URL-адреси:","text-secret-create-disabled":"Створення нових секретів у цьому екзепмлярі вимкнено.","title-explanation":"Ось як це працює\u0026hellip;","title-new-secret":"Створіть новий секрет","title-reading-secret":"Читання вашого секрету\u0026hellip;","title-secret-create-disabled":"Створення секрету вимкнено…","title-secret-created":"Секрет створений!"}'), + 'zh': JSON.parse('{"alert-secret-not-found":"这不是您正在寻找的机密\u0026hellip; - 如果您期望机密会出现在这里,它可能已经被泄漏了,因为可能有其他人已经打开了此链接。","alert-something-went-wrong":"看样子出了一些问题,对此我非常抱歉\u0026hellip;","btn-create-secret":"创建机密!","btn-new-secret":"新的机密","btn-reveal-secret":"向我展示机密!","btn-show-explanation":"这是如何工作的?","items-explanation":["您在当前页面上的字段输入一个机密","您的浏览器使用生成的密码加密刚才的机密","只有加密后的机密被发送到服务器(无论是机密的明文内容还是加密的密码都不会被发送!)","服务器将加密的机密存储一定时间","您将显示的包含 ID 和解密密码的 URL 链接发送给收件人","收件人只能查看一次机密:如果他们无法查看,就代表这个机密已经被其他人看过来!","当加密的机密被取回一次以后,他将从服务器上被删除"],"label-secret-data":"机密数据","text-burn-hint":"请注意您自己不要访问这个 URL 地址,否则这会破坏机密。 就这样把它发送给别人就可以了!","text-hint-burned":"\u003cstrong\u003e请注意:\u003c/strong\u003e 您只能看到这一次! 一旦您刷新当前页面,机密就会消失,所以应该现在就复制机密\u0026hellip;","text-powered-by":"强力驱动通过","text-pre-reveal-hint":"要揭露这个机密,请点击此按钮,但注意这样做之后会破坏此机密。 您只能查看一次!","text-pre-url":"您的机密是使用此 URL 创建和存储:","title-explanation":"这就是它的工作方式\u0026hellip;","title-new-secret":"创建一个机密","title-reading-secret":"读取你的机密\u0026hellip;","title-secret-created":"机密已创建!"}'), + 'zh-TW': JSON.parse('{"alert-secret-not-found":"這不是您正在尋找的機密\u0026hellip; - 如果您期望機密會出現在這裡,它可能已經被泄漏了,因為可能有其他人已經打開了此連結。","alert-something-went-wrong":"看樣子出了一些問題,對此我非常抱歉\u0026hellip;","btn-create-secret":"創建機密!","btn-new-secret":"新的機密","btn-reveal-secret":"向我展示機密!","btn-show-explanation":"這是如何工作的?","items-explanation":["您在當前頁面上的欄位輸入一個機密","您的瀏覽器使用生成的密碼加密剛才的機密","只有加密後的機密被發送到伺服器(無論是機密的明文內容還是加密的密碼都不會被發送!)","伺服器將加密的機密存儲一定時間","您將顯示的包含 ID 和解密密碼的 URL 連結發送給收件人","收件人只能查看一次機密:如果他們無法查看,就代表這個機密已經被其他人看過來!","當加密的機密被取回一次以後,他將從伺服器上被刪除"],"label-secret-data":"機密數據","text-burn-hint":"請注意您自己不要訪問這個 URL 地址,否則這會破壞機密。 就這樣把它發送給別人就可以了!","text-hint-burned":"\u003cstrong\u003e請注意:\u003c/strong\u003e 您只能看到這一次! 一旦您刷新當前頁面,機密就會消失,所以應該現在就複製機密\u0026hellip;","text-powered-by":"強力驅動通過","text-pre-reveal-hint":"要揭露這個機密,請點擊此按鈕,但注意這樣做之後會破壞此機密。 您只能查看一次!","text-pre-url":"您的機密是使用此 URL 創建和存儲:","title-explanation":"這就是它的工作方式\u0026hellip;","title-new-secret":"創建一個機密","title-reading-secret":"讀取你的機密\u0026hellip;","title-secret-created":"機密已創建!"}'), + } diff --git a/src/langs/langs.tpl.js b/src/langs/langs.tpl.js new file mode 100644 index 0000000..61a2d84 --- /dev/null +++ b/src/langs/langs.tpl.js @@ -0,0 +1,18 @@ +// Auto-Generated, do not edit! + +const switchFormal = (formal, informal) => { + return window.useFormalLanguage ? formal : informal +} + +export default { +{{- range $lang, $translation := .Translations -}} +{{- if .FormalTranslations }} + '{{ $lang }}': switchFormal( + JSON.parse('{{ mustMergeOverwrite (dict) .Translations .FormalTranslations | mustToJson | replace "'" "\\'" }}'), + JSON.parse('{{ .Translations | mustToJson | replace "'" "\\'" }}'), + ), +{{ else }} + '{{ $lang }}': JSON.parse('{{ .Translations | mustToJson | replace "'" "\\'" }}'), +{{ end -}} +{{- end }} +} diff --git a/tplFuncs.go b/tplFuncs.go index 3e90b4d..d82e63e 100644 --- a/tplFuncs.go +++ b/tplFuncs.go @@ -5,16 +5,20 @@ import ( "encoding/base64" "sync" "text/template" + + "github.com/Masterminds/sprig/v3" ) var ( sriCacheStore = newSRICache() - tplFuncs = template.FuncMap{ - "list": func(args ...string) []string { return args }, - "assetSRI": assetSRIHash, - } + tplFuncs template.FuncMap ) +func init() { + tplFuncs = sprig.FuncMap() + tplFuncs["assetSRI"] = assetSRIHash +} + func assetSRIHash(assetName string) string { if sri, ok := sriCacheStore.Get(assetName); ok { return sri