* [MM-59089] Add a compliance export constant (#27919) * add a useful constant * i18n * another constant * another i18n * [MM-60422] Add GetChannelsWithActivityDuring (#28301) * modify GetUsersInChannelDuring to accept a slice of channelIds * add GetChannelsWithActivityDuring * add compliance export progress message; remove unused custom status * linting * tests running too fast * add batch size config settings * add store tests * linting * empty commit * i18n changes * fix i18n ordering * MM-60570 - Server-side changes consolidating the export CLI with server/ent code (#28640) * add an i18n field; add the CLI's export directory * int64 -> int * Add UntilUpdateAt for MessageExport and AnalyticsPostCount to merge * remove now-unused i18n strings * add TranslationsPreInitFromBuffer to allow CLI to use i18n * use GetBuilder to simplify; rename TranslationsPreInitFromFileBytes * [MM-59089] Improve compliance export timings (#1733 - Enterprise repo) * MM-60422 - Performance and logic fixes for Compliance Exports (#1757 - Enterprise repo) * MM-60570 - Enterprise-side changes consolidating the export CLI with server/ent code (#1769 - Enterprise repo) * merge conflicts; missed file from ent branch * MM-61038 - Add an option to sqlstore.New (#28702) remove useless comment add test add an option to sqlstore.New * MM-60976: Remove RunExport command from Mattermost binary (#28805) * remove RunExport command from mattermost binary * remove the code it was calling * fix i18n * remove test (was only testing license, not functionality) * empty commit * fix flaky GetChannelsWithActivityDuring test * MM-60063: Dedicated Export Filestore fix, redo of #1772 (enterprise) (#28803) * redo filestore fix #1772 (enterprise repo) on top of MM-59966 feature * add new e2e tests for export filestore * golint * ok, note to self: shadowing bad, actually (when there's a defer) * empty commit * MM-61137 - Message export: Support 7.8.11 era dbs (#28824) * support 7.8.11 era dbs by wrapping the store using only what we need * fix flaky GetChannelsWithActivityDuring test * add a comment * only need to define the MEFileInfoStore (the one that'll be overridden) * blank commit * MM-60974 - Message Export: Add performance metrics (#28836) * support 7.8.11 era dbs by wrapping the store using only what we need * fix flaky GetChannelsWithActivityDuring test * add a comment * only need to define the MEFileInfoStore (the one that'll be overridden) * performance metrics * cleanup unneeded named returns * blank commit * MM-60975 - Message export: Add startTime and endTime to export folder name (#28840) * support 7.8.11 era dbs by wrapping the store using only what we need * fix flaky GetChannelsWithActivityDuring test * add a comment * only need to define the MEFileInfoStore (the one that'll be overridden) * performance metrics * output startTime and endTime in export folder * empty commit * merge conflict * MM-60978 - Message export: Improve xml fields; fix delete semantics (#28873) * support 7.8.11 era dbs by wrapping the store using only what we need * fix flaky GetChannelsWithActivityDuring test * add a comment * only need to define the MEFileInfoStore (the one that'll be overridden) * performance metrics * output startTime and endTime in export folder * empty commit * add xml fields, omit when empty, tests * fix delete semantics; test (and test for update semantics) * clarify comments * simplify edited post detection, now there's no edge case. * add some spacing to help fast running tests * merge conflicts/updates needed for new deleted post semantics * linting; fixing tests from upstream merge * use SafeDereference * linting * stronger typing; better wrapped errors; better formatting * blank commit * goimports formatting * fix merge mistake * minor fixes due to changes in master * MM-61755 - Simplifying and Support reporting to the db from the CLI (#29281) * finally clean up JobData struct and stringMap; prep for CLI using db * and now simplify using StringMapToJobDataWithZeroValues * remove unused fn * create JobDataExported; clean up errors * MM-60176 - Message Export: Global relay cleanup (#29168) * move global relay logic into global_relay_export * blank commit * blank commit * improve errors * MM-60693 - Refactor CSV to use same codepath as Actiance (#29191) * move global relay logic into global_relay_export * blank commit * refactor (and simplify) ExportParams into shared * blank commit * remove unused fn * csv now uses pre-calculated joins/leaves like actiance * improve errors * remove nil post check; remove ignoredPosts metric * remove unneeded copy * MM-61696 - Refactor GlobalRelay to use same codepath as Actiance (#29225) * move global relay logic into global_relay_export * blank commit * refactor (and simplify) ExportParams into shared * blank commit * remove unused fn * csv now uses pre-calculated joins/leaves like actiance * remove newly unneeded function and its test. goodbye. * refactor GetPostAttachments for csv + global relay to share * refactor global_relay_export and fix tests (no changes to output) * improve errors * remove nil post check; remove ignoredPosts metric * remove unneeded copy * remove unneeded nil check * PR comments * MM-61715 - Generalize e2e to all export types 🤖 (#29369) * move global relay logic into global_relay_export * blank commit * refactor (and simplify) ExportParams into shared * blank commit * remove unused fn * csv now uses pre-calculated joins/leaves like actiance * remove newly unneeded function and its test. goodbye. * refactor GetPostAttachments for csv + global relay to share * refactor global_relay_export and fix tests (no changes to output) * improve errors * remove nil post check; remove ignoredPosts metric * remove unneeded copy * remove unneeded nil check * PR comments * refactor isDeletedMsg for all export types * fix start and endtime, nasty csv createAt bug; bring closer to Actiance * align unit tests with new logic (e.g. starttime / endtime) * refactor a TimestampConvert fn for code + tests * bug: pass templates to global relay (hurray for e2e tests, otherwise...) * add global relay zip to allowed list (only for tests) * test helpers * new templates for e2e tests * e2e tests... phew. * linting * merge conflicts * unexport PostToRow; add test helper marker * cleanup, shortening, thanks to PR comments * MM-61972 - Generalize export data path - Actiance (#29399) * extract and generalize the export data generation functions * finish moving test (bc of previous extraction) * lift a function from common -> shared (to break an import cycle) * actiance now takes general export data, processes it into actiance data * bring tests in line with correct sorting rules (upadateAt, messageId) * fixups, PR comments * turn strings.Repeat into a more descriptive const amended: one letter fix; bad rebase * MM-62009 - e2e clock heisenbug (#29434) * consolidate assertions; output debuggable diffs (keeping for future) * refactor test output generator to generators file * waitUntilZeroPosts + pass through until to job = fix all clock issues * simplify messages to model.NewId(); remove unneeded waitUntilZeroPosts * model.NewId() -> storetest.NewTestID() * MM-61980 - Generalize export data path - CSV (#29482) * simple refactoring * increase sleep times for (very) rare test failures * add extra information to the generic export for CSV * adj Actiance to handle new generic export (no difference in its output) * no longer need mergePosts (yay), move getJoinLeavePosts for everyone * adjust tests for new csv semantics (detailed in summary) * and need to add the new exported data to the export_data_tests * rearrange csv writing to happen after data export (more logical) * linting * remove debug statements * figured out what was wrong with global relay e2e test 3; solid now * PR comments * MM-61718 - Generalize export data path - Global Relay (#29508) * move global relay over to using the generalized export data * performance pass -- not much can be done * Update server/enterprise/message_export/global_relay_export/global_relay_export.go Co-authored-by: Claudio Costa <cstcld91@gmail.com> --------- Co-authored-by: Claudio Costa <cstcld91@gmail.com> * MM-62058 - Align CSV with Actiance (#29551) * refactoring actiance files and var names for clarity * bug found in exported attachments (we used to miss some start/ends) * changes needed for actiance due to new generic exports * bringing CSV up to actiance standards * fixing global relay b/c of new semantics (adding a note on an edge case) * aligning e2e tests, adding comments to clarify what is expected/tested * necessary changes; 1 more test for added functionality (ignoreDeleted) * comment style * MM-62059 - Align Global Relay with Actiance/CSV; many fixes (#29665) * core logic changes to general export_data and the specific export paths * unit tests and e2e tests, covering all new edge cases and all logic * linting * better var naming, const value, and cleaning up functions calls * MM-62436 - Temporarily skip cypress tests that require download link (#29772) --------- Co-authored-by: Claudio Costa <cstcld91@gmail.com>
300 строки
8.4 KiB
Go
300 строки
8.4 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package i18n
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/mattermost/go-i18n/i18n"
|
|
"github.com/mattermost/go-i18n/i18n/bundle"
|
|
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
)
|
|
|
|
const defaultLocale = "en"
|
|
|
|
// TranslateFunc is the type of the translate functions
|
|
type TranslateFunc func(translationID string, args ...any) string
|
|
|
|
// TranslationFuncByLocal is the type of function that takes local as a string and returns the translation function
|
|
type TranslationFuncByLocal func(locale string) TranslateFunc
|
|
|
|
// T is the translate function using the default server language as fallback language
|
|
var T TranslateFunc
|
|
|
|
// TDefault is the translate function using english as fallback language
|
|
var TDefault TranslateFunc
|
|
|
|
var locales = make(map[string]string)
|
|
|
|
// supportedLocales is a hard-coded list of locales considered ready for production use. It must
|
|
// be kept in sync with ../../../../webapp/channels/src/i18n/i18n.jsx.
|
|
var supportedLocales = []string{
|
|
"de",
|
|
"en",
|
|
"en-AU",
|
|
"es",
|
|
"fr",
|
|
"it",
|
|
"hu",
|
|
"nl",
|
|
"pl",
|
|
"pt-BR",
|
|
"ro",
|
|
"sv",
|
|
"vi",
|
|
"tr",
|
|
"bg",
|
|
"ru",
|
|
"uk",
|
|
"fa",
|
|
"ko",
|
|
"zh-CN",
|
|
"zh-TW",
|
|
"ja",
|
|
}
|
|
var defaultServerLocale string
|
|
var defaultClientLocale string
|
|
|
|
// TranslationsPreInit loads translations from filesystem if they are not
|
|
// loaded already and assigns english while loading server config
|
|
func TranslationsPreInit(translationsDir string) error {
|
|
if T != nil {
|
|
return nil
|
|
}
|
|
|
|
// Set T even if we fail to load the translations. Lots of shutdown handling code will
|
|
// segfault trying to handle the error, and the untranslated IDs are strictly better.
|
|
T = tfuncWithFallback(defaultLocale)
|
|
TDefault = tfuncWithFallback(defaultLocale)
|
|
|
|
return initTranslationsWithDir(translationsDir)
|
|
}
|
|
|
|
// TranslationsPreInitFromFileBytes loads translations from a buffer -- useful if
|
|
// we need to initialize i18n from an embedded i18n file (e.g., from a CLI tool)
|
|
func TranslationsPreInitFromFileBytes(filename string, buf []byte) error {
|
|
if T != nil {
|
|
return nil
|
|
}
|
|
|
|
// Set T even if we fail to load the translations. Lots of shutdown handling code will
|
|
// segfault trying to handle the error, and the untranslated IDs are strictly better.
|
|
T = tfuncWithFallback(defaultLocale)
|
|
TDefault = tfuncWithFallback(defaultLocale)
|
|
|
|
locale := strings.Split(filename, ".")[0]
|
|
if !isSupportedLocale(locale) {
|
|
return fmt.Errorf("locale not supported: %s", locale)
|
|
}
|
|
|
|
locales[locale] = filename
|
|
|
|
return i18n.ParseTranslationFileBytes(filename, buf)
|
|
}
|
|
|
|
// InitTranslations set the defaults configured in the server and initialize
|
|
// the T function using the server default as fallback language
|
|
func InitTranslations(serverLocale, clientLocale string) error {
|
|
defaultServerLocale = serverLocale
|
|
defaultClientLocale = clientLocale
|
|
|
|
var err error
|
|
T, err = GetTranslationsBySystemLocale()
|
|
return err
|
|
}
|
|
|
|
func initTranslationsWithDir(dir string) error {
|
|
files, _ := os.ReadDir(dir)
|
|
for _, f := range files {
|
|
if filepath.Ext(f.Name()) == ".json" {
|
|
filename := f.Name()
|
|
|
|
locale := strings.Split(filename, ".")[0]
|
|
if !isSupportedLocale(locale) {
|
|
continue
|
|
}
|
|
|
|
locales[locale] = filepath.Join(dir, filename)
|
|
|
|
if err := i18n.LoadTranslationFile(filepath.Join(dir, filename)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetTranslationFuncForDir loads translations from the filesystem into a new instance of the bundle.
|
|
// It returns a function to access loaded translations.
|
|
func GetTranslationFuncForDir(dir string) (TranslationFuncByLocal, error) {
|
|
var availableLocals = make(map[string]string)
|
|
bundle := bundle.New()
|
|
files, _ := os.ReadDir(dir)
|
|
for _, f := range files {
|
|
if filepath.Ext(f.Name()) != ".json" {
|
|
continue
|
|
}
|
|
|
|
locale := strings.Split(f.Name(), ".")[0]
|
|
if !isSupportedLocale(locale) {
|
|
continue
|
|
}
|
|
|
|
filename := f.Name()
|
|
availableLocals[locale] = filepath.Join(dir, filename)
|
|
if err := bundle.LoadTranslationFile(filepath.Join(dir, filename)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return func(locale string) TranslateFunc {
|
|
if _, ok := availableLocals[locale]; !ok {
|
|
locale = defaultLocale
|
|
}
|
|
|
|
t, _ := bundle.Tfunc(locale)
|
|
return func(translationID string, args ...any) string {
|
|
if translated := t(translationID, args...); translated != translationID {
|
|
return translated
|
|
}
|
|
|
|
t, _ := bundle.Tfunc(defaultLocale)
|
|
return t(translationID, args...)
|
|
}
|
|
}, nil
|
|
}
|
|
|
|
func GetTranslationsBySystemLocale() (TranslateFunc, error) {
|
|
locale := defaultServerLocale
|
|
if _, ok := locales[locale]; !ok {
|
|
mlog.Warn("Failed to load system translations for selected locale, attempting to fall back to default", mlog.String("locale", locale), mlog.String("default_locale", defaultLocale))
|
|
locale = defaultLocale
|
|
}
|
|
|
|
if !isSupportedLocale(locale) {
|
|
mlog.Warn("Selected locale is unsupported, attempting to fall back to default", mlog.String("locale", locale), mlog.String("default_locale", defaultLocale))
|
|
locale = defaultLocale
|
|
}
|
|
|
|
if locales[locale] == "" {
|
|
return nil, fmt.Errorf("failed to load system translations for '%v'", defaultLocale)
|
|
}
|
|
|
|
translations := tfuncWithFallback(locale)
|
|
if translations == nil {
|
|
return nil, fmt.Errorf("failed to load system translations")
|
|
}
|
|
|
|
mlog.Info("Loaded system translations", mlog.String("for locale", locale), mlog.String("from locale", locales[locale]))
|
|
return translations, nil
|
|
}
|
|
|
|
// GetUserTranslations get the translation function for an specific locale
|
|
func GetUserTranslations(locale string) TranslateFunc {
|
|
if _, ok := locales[locale]; !ok {
|
|
locale = defaultLocale
|
|
}
|
|
|
|
translations := tfuncWithFallback(locale)
|
|
return translations
|
|
}
|
|
|
|
// GetTranslationsAndLocaleFromRequest return the translation function and the
|
|
// locale based on a request headers
|
|
func GetTranslationsAndLocaleFromRequest(r *http.Request) (TranslateFunc, string) {
|
|
// This is for checking against locales like pt_BR or zn_CN
|
|
headerLocaleFull := strings.Split(r.Header.Get("Accept-Language"), ",")[0]
|
|
// This is for checking against locales like en, es
|
|
headerLocale := strings.Split(strings.Split(r.Header.Get("Accept-Language"), ",")[0], "-")[0]
|
|
defaultLocale := defaultClientLocale
|
|
if locales[headerLocaleFull] != "" {
|
|
translations := tfuncWithFallback(headerLocaleFull)
|
|
return translations, headerLocaleFull
|
|
} else if locales[headerLocale] != "" {
|
|
translations := tfuncWithFallback(headerLocale)
|
|
return translations, headerLocale
|
|
} else if locales[defaultLocale] != "" {
|
|
translations := tfuncWithFallback(defaultLocale)
|
|
return translations, headerLocale
|
|
}
|
|
|
|
translations := tfuncWithFallback(defaultLocale)
|
|
return translations, defaultLocale
|
|
}
|
|
|
|
// GetSupportedLocales return a map of locale code and the file path with the
|
|
// translations
|
|
func GetSupportedLocales() map[string]string {
|
|
return locales
|
|
}
|
|
|
|
func tfuncWithFallback(pref string) TranslateFunc {
|
|
t, _ := i18n.Tfunc(pref)
|
|
return func(translationID string, args ...any) string {
|
|
if translated := t(translationID, args...); translated != translationID {
|
|
return translated
|
|
}
|
|
|
|
t, _ := i18n.Tfunc(defaultLocale)
|
|
return t(translationID, args...)
|
|
}
|
|
}
|
|
|
|
// TranslateAsHTML translates the translationID provided and return a
|
|
// template.HTML object
|
|
func TranslateAsHTML(t TranslateFunc, translationID string, args map[string]any) template.HTML {
|
|
message := t(translationID, escapeForHTML(args))
|
|
message = strings.Replace(message, "[[", "<strong>", -1)
|
|
message = strings.Replace(message, "]]", "</strong>", -1)
|
|
return template.HTML(message)
|
|
}
|
|
|
|
func escapeForHTML(arg any) any {
|
|
switch typedArg := arg.(type) {
|
|
case string:
|
|
return template.HTMLEscapeString(typedArg)
|
|
case *string:
|
|
return template.HTMLEscapeString(*typedArg)
|
|
case map[string]any:
|
|
safeArg := make(map[string]any, len(typedArg))
|
|
for key, value := range typedArg {
|
|
safeArg[key] = escapeForHTML(value)
|
|
}
|
|
return safeArg
|
|
default:
|
|
mlog.Warn(
|
|
"Unable to escape value for HTML template",
|
|
mlog.Any("html_template", arg),
|
|
mlog.String("template_type", reflect.ValueOf(arg).Type().String()),
|
|
)
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// IdentityTfunc returns a translation function that don't translate, only
|
|
// returns the same id
|
|
func IdentityTfunc() TranslateFunc {
|
|
return func(translationID string, args ...any) string {
|
|
return translationID
|
|
}
|
|
}
|
|
|
|
func isSupportedLocale(locale string) bool {
|
|
for _, supportedLocale := range supportedLocales {
|
|
if locale == supportedLocale {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|