├── .babelrc
├── .gitignore
├── LICENSE.txt
├── README.md
├── backends
├── database
│ ├── database.go
│ └── database_test.go
└── yaml
│ ├── tests
│ ├── chinese.yml
│ ├── english.yaml
│ └── subdir
│ │ ├── german.yml
│ │ └── no_language_file.txt
│ ├── yaml.go
│ └── yaml_test.go
├── controller.go
├── exchange_actions
├── exchange_actions.go
├── exchange_actions_test.go
├── fixtures
│ ├── export_all.csv
│ ├── export_backend.csv
│ └── export_frontend.csv
├── public
│ └── imports
│ │ ├── import_1.csv
│ │ ├── import_2.csv
│ │ └── import_3.csv
└── views
│ └── themes
│ └── i18n
│ └── actions
│ └── index
│ └── exchange.tmpl
├── i18n.go
├── i18n_test.go
├── inline_edit
├── inline_edit.go
└── views
│ └── themes
│ └── i18n
│ └── assets
│ ├── javascripts
│ ├── i18n-checker.js
│ ├── i18n-inline.js
│ └── i18n-inline
│ │ ├── 1.poshytip.min.js
│ │ ├── 2.jquery.editable.poshytip.min.js
│ │ └── 3.i18n-inline.js
│ └── stylesheets
│ ├── i18n-inline.css
│ └── i18n-inline
│ └── i18n-inline.scss
├── package.json
├── views
└── themes
│ └── i18n
│ ├── assets
│ ├── javascripts
│ │ ├── i18n.js
│ │ └── i18n
│ │ │ └── i18n.js
│ └── stylesheets
│ │ ├── i18n.css
│ │ └── i18n
│ │ └── i18n.scss
│ └── index.tmpl
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | exchange_actions/public/download*
2 | /public
3 | *.log
4 | *.sqlite3
5 | *.swp
6 | .~lock.*
7 | *.elc
8 | .rbenv-version
9 | tags
10 | TAGS
11 | *#
12 | .#*
13 | *.test
14 | .DS_Store
15 | .otto
16 | .ottoid
17 | gin-bin
18 | *.js.map
19 | node_modules/
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) The Plant https://theplant.jp
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # I18n
2 |
3 | I18n provides internationalization support for your application, it supports 2 kinds of storages(backends), the database and file system.
4 |
5 | [](https://godoc.org/github.com/qor/i18n)
6 |
7 | ## Usage
8 |
9 | Initialize I18n with the storage mode. You can use both storages together, the earlier one has higher priority. So in the example, I18n will look up the translation in database first, then continue finding it in the YAML file if not found.
10 |
11 | ```go
12 | import (
13 | "github.com/jinzhu/gorm"
14 | "github.com/qor/i18n"
15 | "github.com/qor/i18n/backends/database"
16 | "github.com/qor/i18n/backends/yaml"
17 | )
18 |
19 | func main() {
20 | db, _ := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
21 |
22 | I18n := i18n.New(
23 | database.New(&db), // load translations from the database
24 | yaml.New(filepath.Join(config.Root, "config/locales")), // load translations from the YAML files in directory `config/locales`
25 | )
26 |
27 | I18n.T("en-US", "demo.greeting") // Not exist at first
28 | I18n.T("en-US", "demo.hello") // Exists in the yml file
29 |
30 | i18n.Default = "zh-CN" // change the default locale. the original value is "en-US"
31 | }
32 | ```
33 |
34 | Once a database has been set for I18n, all **untranslated** translations inside `I18n.T()` will be loaded into `translations` table in the database when compiling the application. For example, we have an untranslated `I18n.T("en-US", "demo.greeting")` in the example, so I18n will generate this record in the `translations` table after compiling.
35 |
36 | | locale | key | value |
37 | | --- | --- | --- |
38 | | en-US | demo.greeting | |
39 |
40 | The YAML file format is
41 |
42 | ```yaml
43 | en-US:
44 | demo:
45 | hello: "Hello, world"
46 | ```
47 |
48 | ### Use built-in interface for translation management with [QOR Admin](http://github.com/qor/admin)
49 |
50 | I18n has a built-in web interface for translations which is integrated with [QOR Admin](http://github.com/qor/admin).
51 |
52 | ```go
53 | Admin.AddResource(I18n)
54 | ```
55 |
56 | To let users able to translate between locales in the admin interface, your "User" need to implement these interfaces.
57 | ```go
58 | func (user User) EditableLocales() []string {
59 | return []string{"en-US", "zh-CN"}
60 | }
61 |
62 | func (user User) ViewableLocales() []string {
63 | return []string{"en-US", "zh-CN"}
64 | }
65 | ```
66 |
67 | Refer the [online demo](http://demo.getqor.com/admin/translations).
68 |
69 | ### Use with Golang templates
70 |
71 | The easy way to use I18n in a template is to define a `t` function and register it as `FuncMap`:
72 |
73 | ```go
74 | func T(key string, value string, args ...interface{}) string {
75 | return I18n.Default(value).T("en-US", key, args...)
76 | }
77 |
78 | // then use it in the template
79 | {{ t "demo.greet" "Hello, {{$1}}" "John" }} // -> Hello, John
80 | ```
81 |
82 | ### Built-in functions for translations management
83 |
84 | I18n has functions to manage translation directly.
85 |
86 | ```go
87 | // Add Translation
88 | I18n.AddTranslation(&i18n.Translation{Key: "hello-world", Locale: "en-US", Value: "hello world"})
89 |
90 | // Update Translation
91 | I18n.SaveTranslation(&i18n.Translation{Key: "hello-world", Locale: "en-US", Value: "Hello World"})
92 |
93 | // Delete Translation
94 | I18n.DeleteTranslation(&i18n.Translation{Key: "hello-world", Locale: "en-US", Value: "Hello World"})
95 | ```
96 |
97 | ### Scope and default value
98 |
99 | Call Translation with `Scope` or set default value.
100 |
101 | ```go
102 | // Read Translation with `Scope`
103 | I18n.Scope("home-page").T("zh-CN", "hello-world") // read translation with translation key `home-page.hello-world`
104 |
105 | // Read Translation with `Default Value`
106 | I18n.Default("Default Value").T("zh-CN", "non-existing-key") // Will return default value `Default Value`
107 | ```
108 |
109 | ### Fallbacks
110 |
111 | I18n has a `Fallbacks` function to register fallbacks. For example, registering `en-GB` as a fallback to `zh-CN`:
112 |
113 | ```go
114 | i18n := New(&backend{})
115 | i18n.AddTranslation(&Translation{Key: "hello-world", Locale: "en-GB", Value: "Hello World"})
116 |
117 | fmt.Print(i18n.Fallbacks("en-GB").T("zh-CN", "hello-world")) // "Hello World"
118 | ```
119 |
120 | **To set fallback [*Locale*](https://en.wikipedia.org/wiki/Locale_(computer_software)) globally** you can use `I18n.FallbackLocales`. This function accepts a `map[string][]string` as parameter. The key is the fallback *Locale* and the `[]string` is the *Locales* that could fallback to the first *Locale*.
121 |
122 | For example, setting `"fr-FR", "de-DE", "zh-CN"` fallback to `en-GB` globally:
123 |
124 | ```go
125 | I18n.FallbackLocales = map[string][]string{"en-GB": []{"fr-FR", "de-DE", "zh-CN"}}
126 | ```
127 |
128 | ### Interpolation
129 |
130 | I18n utilizes a Golang template to parse translations with an interpolation variable.
131 |
132 | ```go
133 | type User struct {
134 | Name string
135 | }
136 |
137 | I18n.AddTranslation(&i18n.Translation{Key: "hello", Locale: "en-US", Value: "Hello {{.Name}}"})
138 |
139 | I18n.T("en-US", "hello", User{Name: "Jinzhu"}) //=> Hello Jinzhu
140 | ```
141 |
142 | ### Pluralization
143 |
144 | I18n utilizes [cldr](https://github.com/theplant/cldr) to achieve pluralization, it provides the functions `p`, `zero`, `one`, `two`, `few`, `many`, `other` for this purpose. Please refer to [cldr documentation](https://github.com/theplant/cldr) for more information.
145 |
146 | ```go
147 | I18n.AddTranslation(&i18n.Translation{Key: "count", Locale: "en-US", Value: "{{p "Count" (one "{{.Count}} item") (other "{{.Count}} items")}}"})
148 | I18n.T("en-US", "count", map[string]int{"Count": 1}) //=> 1 item
149 | ```
150 |
151 | ### Ordered Params
152 |
153 | ```go
154 | I18n.AddTranslation(&i18n.Translation{Key: "ordered_params", Locale: "en-US", Value: "{{$1}} {{$2}} {{$1}}"})
155 | I18n.T("en-US", "ordered_params", "string1", "string2") //=> string1 string2 string1
156 | ```
157 |
158 | ### Inline Edit
159 |
160 | You could manage translations' data with [QOR Admin](http://github.com/qor/admin) interface (UI) after registering it into [QOR Admin](http://github.com/qor/admin), however we warn you that it is usually quite hard (and error prone!) to *translate a translation* without knowing its context...Fortunately, the *Inline Edit* feature of [QOR Admin](http://github.com/qor/admin) was developed to resolve this problem!
161 |
162 | *Inline Edit* allows administrators to manage translations from the frontend. Similarly to [integrating with Golang Templates](#integrate-with-golang-templates), you need to register a func map for Golang templates to render *inline editable* translations.
163 |
164 | The good thing is we have created a package for you to do this easily, it will generate a `FuncMap`, you just need to use it when parsing your templates:
165 |
166 | ```go
167 | // `I18n` hold translations backends
168 | // `en-US` current locale
169 | // `true` enable inline edit mode or not, if inline edit not enabled, it works just like the funcmap in section "Integrate with Golang Templates"
170 | inline_edit.FuncMap(I18n, "en-US", true) // => map[string]interface{}{
171 | // "t": func(string, ...interface{}) template.HTML {
172 | // // ...
173 | // },
174 | // }
175 | ```
176 |
177 |
178 |
179 | ## License
180 |
181 | Released under the [MIT License](http://opensource.org/licenses/MIT).
182 |
--------------------------------------------------------------------------------
/backends/database/database.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/jinzhu/gorm"
7 | "github.com/qor/i18n"
8 | )
9 |
10 | // Translation is a struct used to save translations into databae
11 | type Translation struct {
12 | Locale string `sql:"size:12;"`
13 | Key string `sql:"size:4294967295;"`
14 | Value string `sql:"size:4294967295"`
15 | }
16 |
17 | // New new DB backend for I18n
18 | func New(db *gorm.DB) i18n.Backend {
19 | db.AutoMigrate(&Translation{})
20 | if err := db.Model(&Translation{}).AddUniqueIndex("idx_translations_key_with_locale", "locale", "key").Error; err != nil {
21 | fmt.Printf("Failed to create unique index for translations key & locale, got: %v\n", err.Error())
22 | }
23 | return &Backend{DB: db}
24 | }
25 |
26 | // Backend DB backend
27 | type Backend struct {
28 | DB *gorm.DB
29 | }
30 |
31 | // LoadTranslations load translations from DB backend
32 | func (backend *Backend) LoadTranslations() (translations []*i18n.Translation) {
33 | backend.DB.Find(&translations)
34 | return translations
35 | }
36 |
37 | // SaveTranslation save translation into DB backend
38 | func (backend *Backend) SaveTranslation(t *i18n.Translation) error {
39 | return backend.DB.Where(Translation{Key: t.Key, Locale: t.Locale}).
40 | Assign(Translation{Value: t.Value}).
41 | FirstOrCreate(&Translation{}).Error
42 | }
43 |
44 | // FindTranslation find translation from DB backend
45 | func (backend *Backend) FindTranslation(t *i18n.Translation) (translation i18n.Translation) {
46 | backend.DB.Where(Translation{Key: t.Key, Locale: t.Locale}).Find(&translation)
47 | return translation
48 | }
49 |
50 | // DeleteTranslation delete translation into DB backend
51 | func (backend *Backend) DeleteTranslation(t *i18n.Translation) error {
52 | return backend.DB.Where(Translation{Key: t.Key, Locale: t.Locale}).Delete(&Translation{}).Error
53 | }
54 |
--------------------------------------------------------------------------------
/backends/database/database_test.go:
--------------------------------------------------------------------------------
1 | package database_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/jinzhu/gorm"
7 | "github.com/qor/i18n"
8 | "github.com/qor/i18n/backends/database"
9 | "github.com/qor/qor/test/utils"
10 | )
11 |
12 | var db *gorm.DB
13 | var backend i18n.Backend
14 |
15 | func init() {
16 | db = utils.TestDB()
17 | db.DropTable(&database.Translation{})
18 | backend = database.New(db)
19 | }
20 |
21 | func TestTranslations(t *testing.T) {
22 | translation := i18n.Translation{Key: "hello_world", Value: "Hello World", Locale: "zh-CN"}
23 |
24 | backend.SaveTranslation(&translation)
25 | if len(backend.LoadTranslations()) != 1 {
26 | t.Errorf("should has only one translation")
27 | }
28 |
29 | backend.DeleteTranslation(&translation)
30 | if len(backend.LoadTranslations()) != 0 {
31 | t.Errorf("should has none translation")
32 | }
33 |
34 | longText := "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
35 |
36 | backend.SaveTranslation(&i18n.Translation{Key: longText + "1", Value: longText, Locale: "zh-CN"})
37 | backend.SaveTranslation(&i18n.Translation{Key: longText + "2", Value: longText, Locale: "zh-CN"})
38 |
39 | if len(backend.LoadTranslations()) != 2 {
40 | t.Errorf("should has two translations")
41 | }
42 |
43 | backend.DeleteTranslation(&i18n.Translation{Key: longText + "1", Value: longText, Locale: "zh-CN"})
44 | if len(backend.LoadTranslations()) != 1 {
45 | t.Errorf("should has one translation left")
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/backends/yaml/tests/chinese.yml:
--------------------------------------------------------------------------------
1 | zh-CN:
2 | hello: 你好
3 | user:
4 | name: "用户名"
5 | email: "邮箱"
6 |
--------------------------------------------------------------------------------
/backends/yaml/tests/english.yaml:
--------------------------------------------------------------------------------
1 | en:
2 | hello: Hello
3 | user:
4 | name: "User Name"
5 | email: "Email"
6 |
--------------------------------------------------------------------------------
/backends/yaml/tests/subdir/german.yml:
--------------------------------------------------------------------------------
1 | de:
2 | hello: Hallo
3 | user:
4 | name: "Benutzername"
5 | email: "E-Mail-Adresse"
6 |
--------------------------------------------------------------------------------
/backends/yaml/tests/subdir/no_language_file.txt:
--------------------------------------------------------------------------------
1 | This is not a language YAML file.
2 |
--------------------------------------------------------------------------------
/backends/yaml/yaml.go:
--------------------------------------------------------------------------------
1 | package yaml
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "os"
9 | "path/filepath"
10 | "strings"
11 |
12 | "github.com/qor/i18n"
13 | "gopkg.in/yaml.v2"
14 | )
15 |
16 | var _ i18n.Backend = &Backend{}
17 |
18 | // New new YAML backend for I18n
19 | func New(paths ...string) *Backend {
20 | backend := &Backend{}
21 |
22 | var files []string
23 | for _, p := range paths {
24 | if file, err := os.Open(p); err == nil {
25 | defer file.Close()
26 | if fileInfo, err := file.Stat(); err == nil {
27 | if fileInfo.IsDir() {
28 | yamlFiles, _ := filepath.Glob(filepath.Join(p, "*.yaml"))
29 | files = append(files, yamlFiles...)
30 |
31 | ymlFiles, _ := filepath.Glob(filepath.Join(p, "*.yml"))
32 | files = append(files, ymlFiles...)
33 | } else if fileInfo.Mode().IsRegular() {
34 | files = append(files, p)
35 | }
36 | }
37 | }
38 | }
39 | for _, file := range files {
40 | if content, err := ioutil.ReadFile(file); err == nil {
41 | backend.contents = append(backend.contents, content)
42 | }
43 | }
44 | return backend
45 | }
46 |
47 | // NewWithWalk has the same functionality as New but uses filepath.Walk to find all the translation files recursively.
48 | func NewWithWalk(paths ...string) i18n.Backend {
49 | backend := &Backend{}
50 |
51 | var files []string
52 | for _, p := range paths {
53 | filepath.Walk(p, func(path string, fileInfo os.FileInfo, err error) error {
54 | if isYamlFile(fileInfo) {
55 | files = append(files, path)
56 | }
57 | return nil
58 | })
59 | }
60 | for _, file := range files {
61 | if content, err := ioutil.ReadFile(file); err == nil {
62 | backend.contents = append(backend.contents, content)
63 | }
64 | }
65 |
66 | return backend
67 | }
68 |
69 | func isYamlFile(fileInfo os.FileInfo) bool {
70 | if fileInfo == nil {
71 | return false
72 | }
73 | return fileInfo.Mode().IsRegular() && (strings.HasSuffix(fileInfo.Name(), ".yml") || strings.HasSuffix(fileInfo.Name(), ".yaml"))
74 | }
75 |
76 | func walkFilesystem(fs http.FileSystem, entry http.File, prefix string) [][]byte {
77 | var (
78 | contents [][]byte
79 | err error
80 | isRoot bool
81 | )
82 | if entry == nil {
83 | if entry, err = fs.Open("/"); err != nil {
84 | return nil
85 | }
86 | isRoot = true
87 | defer entry.Close()
88 | }
89 | fileInfo, err := entry.Stat()
90 | if err != nil {
91 | return nil
92 | }
93 | if !isRoot {
94 | prefix = prefix + fileInfo.Name() + "/"
95 | }
96 | if fileInfo.IsDir() {
97 | if entries, err := entry.Readdir(-1); err == nil {
98 | for _, e := range entries {
99 | if file, err := fs.Open(prefix + e.Name()); err == nil {
100 | defer file.Close()
101 | contents = append(contents, walkFilesystem(fs, file, prefix)...)
102 | }
103 | }
104 | }
105 | } else if isYamlFile(fileInfo) {
106 | if content, err := ioutil.ReadAll(entry); err == nil {
107 | contents = append(contents, content)
108 | }
109 | }
110 | return contents
111 | }
112 |
113 | // NewWithFilesystem initializes a backend that reads translation files from an http.FileSystem.
114 | func NewWithFilesystem(fss ...http.FileSystem) i18n.Backend {
115 | backend := &Backend{}
116 |
117 | for _, fs := range fss {
118 | backend.contents = append(backend.contents, walkFilesystem(fs, nil, "/")...)
119 | }
120 | return backend
121 | }
122 |
123 | // Backend YAML backend
124 | type Backend struct {
125 | contents [][]byte
126 | }
127 |
128 | func loadTranslationsFromYaml(locale string, value interface{}, scopes []string) (translations []*i18n.Translation) {
129 | switch v := value.(type) {
130 | case yaml.MapSlice:
131 | for _, s := range v {
132 | results := loadTranslationsFromYaml(locale, s.Value, append(scopes, fmt.Sprint(s.Key)))
133 | translations = append(translations, results...)
134 | }
135 | default:
136 | var translation = &i18n.Translation{
137 | Locale: locale,
138 | Key: strings.Join(scopes, "."),
139 | Value: fmt.Sprint(v),
140 | }
141 | translations = append(translations, translation)
142 | }
143 | return
144 | }
145 |
146 | // LoadYAMLContent load YAML content
147 | func (backend *Backend) LoadYAMLContent(content []byte) (translations []*i18n.Translation, err error) {
148 | var slice yaml.MapSlice
149 |
150 | if err = yaml.Unmarshal(content, &slice); err == nil {
151 | for _, item := range slice {
152 | translations = append(translations, loadTranslationsFromYaml(item.Key.(string) /* locale */, item.Value, []string{})...)
153 | }
154 | }
155 |
156 | return translations, err
157 | }
158 |
159 | // LoadTranslations load translations from YAML backend
160 | func (backend *Backend) LoadTranslations() (translations []*i18n.Translation) {
161 | for _, content := range backend.contents {
162 | if results, err := backend.LoadYAMLContent(content); err == nil {
163 | translations = append(translations, results...)
164 | } else {
165 | panic(err)
166 | }
167 | }
168 | return translations
169 | }
170 |
171 | // SaveTranslation save translation into YAML backend, not implemented
172 | func (backend *Backend) SaveTranslation(t *i18n.Translation) error {
173 | return errors.New("not implemented")
174 | }
175 |
176 | // FindTranslation find translation from backend
177 | func (backend *Backend) FindTranslation(t *i18n.Translation) (translation i18n.Translation) {
178 | return translation //not implemented
179 | }
180 |
181 | // DeleteTranslation delete translation into YAML backend, not implemented
182 | func (backend *Backend) DeleteTranslation(t *i18n.Translation) error {
183 | return errors.New("not implemented")
184 | }
185 |
--------------------------------------------------------------------------------
/backends/yaml/yaml_test.go:
--------------------------------------------------------------------------------
1 | package yaml_test
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/qor/i18n"
9 | "github.com/qor/i18n/backends/yaml"
10 | )
11 |
12 | var values = map[string][][]string{
13 | "en": {
14 | {"hello", "Hello"},
15 | {"user.name", "User Name"},
16 | {"user.email", "Email"},
17 | },
18 | "de": {
19 | {"hello", "Hallo"},
20 | {"user.name", "Benutzername"},
21 | {"user.email", "E-Mail-Adresse"},
22 | },
23 | "zh-CN": {
24 | {"hello", "你好"},
25 | {"user.name", "用户名"},
26 | {"user.email", "邮箱"},
27 | },
28 | }
29 |
30 | func checkTranslations(translations []*i18n.Translation) error {
31 | for locale, results := range values {
32 | for _, result := range results {
33 | var found bool
34 | for _, translation := range translations {
35 | if (translation.Locale == locale) && (translation.Key == result[0]) && (translation.Value == result[1]) {
36 | found = true
37 | }
38 | }
39 | if !found {
40 | return fmt.Errorf("failed to found translation %v for %v", result[0], locale)
41 | }
42 | }
43 | }
44 | return nil
45 | }
46 |
47 | func TestLoadTranslations(t *testing.T) {
48 | backend := yaml.New("tests", "tests/subdir")
49 | if err := checkTranslations(backend.LoadTranslations()); err != nil {
50 | t.Fatal(err)
51 | }
52 | }
53 |
54 | func TestLoadTranslationsFilesystem(t *testing.T) {
55 | backend := yaml.NewWithFilesystem(http.Dir("./tests"))
56 | if err := checkTranslations(backend.LoadTranslations()); err != nil {
57 | t.Fatal(err)
58 | }
59 | }
60 |
61 | func TestLoadTranslationsWalk(t *testing.T) {
62 | backend := yaml.NewWithWalk("tests")
63 | if err := checkTranslations(backend.LoadTranslations()); err != nil {
64 | t.Fatal(err)
65 | }
66 | }
67 |
68 | var benchmarkResult error
69 |
70 | func BenchmarkLoadTranslations(b *testing.B) {
71 | var backend i18n.Backend
72 | var err error
73 | for i := 0; i < b.N; i++ {
74 | backend = yaml.New("tests", "tests/subdir")
75 | if err = checkTranslations(backend.LoadTranslations()); err != nil {
76 | b.Fatal(err)
77 | }
78 | }
79 | benchmarkResult = err
80 | }
81 |
82 | var benchmarkResult2 error
83 |
84 | func BenchmarkLoadTranslationsWalk(b *testing.B) {
85 | var backend i18n.Backend
86 | var err error
87 | for i := 0; i < b.N; i++ {
88 | backend = yaml.NewWithWalk("tests")
89 | if err = checkTranslations(backend.LoadTranslations()); err != nil {
90 | b.Fatal(err)
91 | }
92 | }
93 | benchmarkResult2 = err
94 | }
95 |
96 | var benchmarkResult3 error
97 |
98 | func BenchmarkLoadTranslationsFilesystem(b *testing.B) {
99 | var backend i18n.Backend
100 | var err error
101 | for i := 0; i < b.N; i++ {
102 | backend = yaml.NewWithFilesystem(http.Dir("./tests"))
103 | if err = checkTranslations(backend.LoadTranslations()); err != nil {
104 | b.Fatal(err)
105 | }
106 | }
107 | benchmarkResult3 = err
108 | }
109 |
--------------------------------------------------------------------------------
/controller.go:
--------------------------------------------------------------------------------
1 | package i18n
2 |
3 | import (
4 | "github.com/qor/admin"
5 | "github.com/qor/qor/utils"
6 | )
7 |
8 | type i18nController struct {
9 | *I18n
10 | }
11 |
12 | func (controller *i18nController) Index(context *admin.Context) {
13 | context.Execute("index", controller.I18n)
14 | }
15 |
16 | func (controller *i18nController) Update(context *admin.Context) {
17 | form := context.Request.Form
18 | translation := Translation{Key: form.Get("Key"), Locale: form.Get("Locale"), Value: utils.HTMLSanitizer.Sanitize(form.Get("Value"))}
19 |
20 | if err := controller.I18n.SaveTranslation(&translation); err == nil {
21 | context.Writer.Write([]byte("OK"))
22 | } else {
23 | context.Writer.WriteHeader(422)
24 | context.Writer.Write([]byte(err.Error()))
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/exchange_actions/exchange_actions.go:
--------------------------------------------------------------------------------
1 | package exchange_actions
2 |
3 | import (
4 | "encoding/csv"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "runtime/debug"
9 | "sort"
10 | "strings"
11 | "time"
12 |
13 | "github.com/qor/admin"
14 | "github.com/qor/i18n"
15 | "github.com/qor/media/oss"
16 | "github.com/qor/worker"
17 | )
18 |
19 | type ExportTranslationArgument struct {
20 | Scope string
21 | }
22 |
23 | type ImportTranslationArgument struct {
24 | TranslationsFile oss.OSS
25 | }
26 |
27 | // RegisterExchangeJobs register i18n jobs into worker
28 | func RegisterExchangeJobs(I18n *i18n.I18n, Worker *worker.Worker) {
29 | if I18n.Resource == nil {
30 | debug.PrintStack()
31 | fmt.Println("I18n should be registered into `Admin` before register jobs")
32 | return
33 | }
34 |
35 | Admin := I18n.Resource.GetAdmin()
36 | Admin.RegisterViewPath("github.com/qor/i18n/exchange_actions/views")
37 |
38 | // Export Translations
39 | exportTranslationResource := Admin.NewResource(&ExportTranslationArgument{})
40 | exportTranslationResource.Meta(&admin.Meta{Name: "Scope", Type: "select_one", Collection: []string{"All", "Backend", "Frontend"}})
41 |
42 | Worker.RegisterJob(&worker.Job{
43 | Name: "Export Translations",
44 | Group: "Export/Import Translations From CSV file",
45 | Resource: exportTranslationResource,
46 | Handler: func(arg interface{}, qorJob worker.QorJobInterface) (err error) {
47 | var (
48 | locales []string
49 | translationKeys []string
50 | translationsMap = map[string]bool{}
51 | filename = fmt.Sprintf("/downloads/translations.%v.csv", time.Now().UnixNano())
52 | fullFilename = filepath.Join("public", filename)
53 | i18nTranslations = I18n.LoadTranslations()
54 | scope = arg.(*ExportTranslationArgument).Scope
55 | )
56 | qorJob.AddLog("Exporting translations...")
57 |
58 | // Sort locales
59 | for locale := range i18nTranslations {
60 | locales = append(locales, locale)
61 | }
62 | sort.Strings(locales)
63 |
64 | // Create download file
65 | if _, err = os.Stat(filepath.Dir(fullFilename)); os.IsNotExist(err) {
66 | err = os.MkdirAll(filepath.Dir(fullFilename), os.ModePerm)
67 | }
68 | csvfile, err := os.OpenFile(fullFilename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
69 | defer csvfile.Close()
70 | if err != nil {
71 | return err
72 | }
73 |
74 | writer := csv.NewWriter(csvfile)
75 |
76 | // Append Headers
77 | writer.Write(append([]string{"Translation Keys"}, locales...))
78 |
79 | // Sort translation keys
80 | for _, locale := range locales {
81 | for key := range i18nTranslations[locale] {
82 | translationsMap[key] = true
83 | }
84 | }
85 |
86 | for key := range translationsMap {
87 | translationKeys = append(translationKeys, key)
88 | }
89 | sort.Strings(translationKeys)
90 |
91 | // Write CSV file
92 | var (
93 | recordCount = len(translationKeys)
94 | perCount = recordCount/20 + 1
95 | processedRecordLogs = []string{}
96 | index = 0
97 | progressCount = 0
98 | )
99 | for _, translationKey := range translationKeys {
100 | // Filter out translation by scope
101 | index++
102 | if scope == "Backend" && !strings.HasPrefix(translationKey, "qor_") {
103 | continue
104 | }
105 | if scope == "Frontend" && strings.HasPrefix(translationKey, "qor_") {
106 | continue
107 | }
108 | var translations = []string{translationKey}
109 | for _, locale := range locales {
110 | var value string
111 | if translation := i18nTranslations[locale][translationKey]; translation != nil {
112 | value = translation.Value
113 | }
114 | translations = append(translations, value)
115 | }
116 | writer.Write(translations)
117 | processedRecordLogs = append(processedRecordLogs, fmt.Sprintf("Exported %v\n", strings.Join(translations, ",")))
118 | if index == perCount {
119 | qorJob.AddLog(strings.Join(processedRecordLogs, ""))
120 | processedRecordLogs = []string{}
121 | progressCount++
122 | qorJob.SetProgress(uint(float32(progressCount) / float32(20) * 100))
123 | index = 0
124 | }
125 | }
126 | writer.Flush()
127 |
128 | qorJob.SetProgressText(fmt.Sprintf("Download exported translations", filename))
129 | return
130 | },
131 | })
132 |
133 | // Import Translations
134 |
135 | Worker.RegisterJob(&worker.Job{
136 | Name: "Import Translations",
137 | Group: "Export/Import Translations From CSV file",
138 | Resource: Admin.NewResource(&ImportTranslationArgument{}),
139 | Handler: func(arg interface{}, qorJob worker.QorJobInterface) (err error) {
140 | importTranslationArgument := arg.(*ImportTranslationArgument)
141 | qorJob.AddLog("Importing translations...")
142 | if csvfile, err := os.Open(filepath.Join("public", importTranslationArgument.TranslationsFile.URL())); err == nil {
143 | reader := csv.NewReader(csvfile)
144 | reader.TrimLeadingSpace = true
145 | if records, err := reader.ReadAll(); err == nil {
146 | if len(records) > 1 && len(records[0]) > 1 {
147 | var (
148 | recordCount = len(records) - 1
149 | perCount = recordCount/20 + 1
150 | processedRecordLogs = []string{}
151 | locales = records[0][1:]
152 | index = 1
153 | )
154 | for _, values := range records[1:] {
155 | logMsg := ""
156 | for idx, value := range values[1:] {
157 | if value == "" {
158 | if values[0] != "" && locales[idx] != "" {
159 | I18n.DeleteTranslation(&i18n.Translation{
160 | Key: values[0],
161 | Locale: locales[idx],
162 | })
163 | logMsg += fmt.Sprintf("%v/%v Deleted %v,%v\n", index, recordCount, locales[idx], values[0])
164 | }
165 | } else {
166 | I18n.SaveTranslation(&i18n.Translation{
167 | Key: values[0],
168 | Locale: locales[idx],
169 | Value: value,
170 | })
171 | logMsg += fmt.Sprintf("%v/%v Imported %v,%v,%v\n", index, recordCount, locales[idx], values[0], value)
172 | }
173 | }
174 | processedRecordLogs = append(processedRecordLogs, logMsg)
175 | if len(processedRecordLogs) == perCount {
176 | qorJob.AddLog(strings.Join(processedRecordLogs, ""))
177 | processedRecordLogs = []string{}
178 | qorJob.SetProgress(uint(float32(index) / float32(recordCount+1) * 100))
179 | }
180 | index++
181 | }
182 | qorJob.AddLog(strings.Join(processedRecordLogs, ""))
183 | }
184 | }
185 | qorJob.AddLog("Imported translations")
186 | }
187 | return
188 | },
189 | })
190 | }
191 |
--------------------------------------------------------------------------------
/exchange_actions/exchange_actions_test.go:
--------------------------------------------------------------------------------
1 | package exchange_actions_test
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "os"
7 | "testing"
8 |
9 | "github.com/fatih/color"
10 | "github.com/jinzhu/gorm"
11 | _ "github.com/mattn/go-sqlite3"
12 | "github.com/qor/admin"
13 | "github.com/qor/i18n"
14 | "github.com/qor/i18n/backends/database"
15 | "github.com/qor/i18n/exchange_actions"
16 | "github.com/qor/media"
17 | "github.com/qor/media/oss"
18 | "github.com/qor/qor"
19 | "github.com/qor/qor/test/utils"
20 | "github.com/qor/worker"
21 | )
22 |
23 | var db *gorm.DB
24 | var Worker *worker.Worker
25 | var I18n *i18n.I18n
26 |
27 | func init() {
28 | db = utils.TestDB()
29 | reset()
30 | }
31 |
32 | func reset() {
33 | db.DropTable(&database.Translation{})
34 | database.New(db)
35 | Admin := admin.New(&qor.Config{DB: db})
36 | Worker = worker.New()
37 | Admin.AddResource(Worker)
38 | I18n = i18n.New(database.New(db))
39 | I18n.SaveTranslation(&i18n.Translation{Key: "qor_admin.title", Value: "title", Locale: "en-US"})
40 | I18n.SaveTranslation(&i18n.Translation{Key: "qor_admin.subtitle", Value: "subtitle", Locale: "en-US"})
41 | I18n.SaveTranslation(&i18n.Translation{Key: "qor_admin.description", Value: "description", Locale: "en-US"})
42 | I18n.SaveTranslation(&i18n.Translation{Key: "header.title", Value: "Header Title", Locale: "en-US"})
43 | exchange_actions.RegisterExchangeJobs(I18n, Worker)
44 | }
45 |
46 | // Test export translations with scope
47 | type testExportWithScopedCase struct {
48 | Scope string
49 | ExpectExportFile string
50 | }
51 |
52 | func TestExportTranslations(t *testing.T) {
53 | reset()
54 | I18n.SaveTranslation(&i18n.Translation{Key: "header.title", Value: "标题", Locale: "zh-CN"})
55 |
56 | testCases := []*testExportWithScopedCase{
57 | &testExportWithScopedCase{Scope: "", ExpectExportFile: "export_all.csv"},
58 | &testExportWithScopedCase{Scope: "All", ExpectExportFile: "export_all.csv"},
59 | &testExportWithScopedCase{Scope: "Backend", ExpectExportFile: "export_backend.csv"},
60 | &testExportWithScopedCase{Scope: "Frontend", ExpectExportFile: "export_frontend.csv"},
61 | }
62 |
63 | for i, testcase := range testCases {
64 | clearDownloadDir()
65 | for _, job := range Worker.Jobs {
66 | if job.Name == "Export Translations" {
67 | job.Handler(&exchange_actions.ExportTranslationArgument{Scope: testcase.Scope}, job.NewStruct().(worker.QorJobInterface))
68 | if downloadedFileContent() != loadFixture(testcase.ExpectExportFile) {
69 | t.Errorf(color.RedString(fmt.Sprintf("\nExchange TestCase #%d: Failure (%s)\n", i+1, "export results are incorrect")))
70 | } else {
71 | color.Green(fmt.Sprintf("Export with scope TestCase #%d: Success\n", i+1))
72 | }
73 | }
74 | }
75 | }
76 | }
77 |
78 | // Test import translations
79 | type testImportTranslationsCase struct {
80 | ImportFileDesc string
81 | ImportFile string
82 | ExpectZhValues map[string]string
83 | }
84 |
85 | func TestImportTranslations(t *testing.T) {
86 | reset()
87 | testCases := []*testImportTranslationsCase{
88 | &testImportTranslationsCase{
89 | ImportFileDesc: "Normal tranlsation file",
90 | ImportFile: "import_1.csv",
91 | ExpectZhValues: map[string]string{"qor_admin.title": "标题", "qor_admin.subtitle": "小标题", "qor_admin.description": "描述", "header.title": "标题"},
92 | },
93 | &testImportTranslationsCase{
94 | ImportFileDesc: "Translation file with missing header.title",
95 | ImportFile: "import_2.csv",
96 | ExpectZhValues: map[string]string{"qor_admin.title": "标题", "qor_admin.subtitle": "小标题", "qor_admin.description": "描述"},
97 | },
98 | &testImportTranslationsCase{
99 | ImportFileDesc: "Translation file with empty column",
100 | ImportFile: "import_3.csv",
101 | ExpectZhValues: map[string]string{"qor_admin.title": "标题", "qor_admin.subtitle": "小标题", "qor_admin.description": "描述", "header.title": "标题"},
102 | },
103 | }
104 |
105 | for i, testCase := range testCases {
106 | for _, job := range Worker.Jobs {
107 | if job.Name == "Import Translations" {
108 | job.Handler(&exchange_actions.ImportTranslationArgument{TranslationsFile: oss.OSS{media.Base{Url: "imports/" + testCase.ImportFile}}}, job.NewStruct().(worker.QorJobInterface))
109 | translations := I18n.LoadTranslations()["zh-CN"]
110 | if len(translations) == 0 {
111 | t.Errorf(color.RedString(fmt.Sprintf("\nImport TestCase #%d: Failure (%s)\n", i+1, "Doesn't have Zh translations")))
112 | }
113 | for key, translation := range translations {
114 | if testCase.ExpectZhValues[key] != translation.Value {
115 | t.Errorf(color.RedString(fmt.Sprintf("\nImport TestCase #%d: Failure (%s)\n", i+1, "Zh translations not match")))
116 | }
117 | }
118 | color.Green(fmt.Sprintf("Import TestCase #%d: Success\n", i+1))
119 | }
120 | }
121 | }
122 | }
123 |
124 | // Helper functions
125 | func clearDownloadDir() {
126 | files, _ := ioutil.ReadDir("./public/downloads")
127 | for _, f := range files {
128 | os.Remove("./public/downloads/" + f.Name())
129 | }
130 | }
131 |
132 | func downloadedFileContent() string {
133 | files, _ := ioutil.ReadDir("./public/downloads")
134 | for _, f := range files {
135 | if content, err := ioutil.ReadFile("./public/downloads/" + f.Name()); err == nil {
136 | return string(content)
137 | }
138 | }
139 | return ""
140 | }
141 |
142 | func loadFixture(fileName string) string {
143 | if content, err := ioutil.ReadFile("./fixtures/" + fileName); err == nil {
144 | return string(content)
145 | }
146 | return ""
147 | }
148 |
--------------------------------------------------------------------------------
/exchange_actions/fixtures/export_all.csv:
--------------------------------------------------------------------------------
1 | Translation Keys,en-US,zh-CN
2 | header.title,Header Title,标题
3 | qor_admin.description,description,
4 | qor_admin.subtitle,subtitle,
5 | qor_admin.title,title,
6 |
--------------------------------------------------------------------------------
/exchange_actions/fixtures/export_backend.csv:
--------------------------------------------------------------------------------
1 | Translation Keys,en-US,zh-CN
2 | qor_admin.description,description,
3 | qor_admin.subtitle,subtitle,
4 | qor_admin.title,title,
5 |
--------------------------------------------------------------------------------
/exchange_actions/fixtures/export_frontend.csv:
--------------------------------------------------------------------------------
1 | Translation Keys,en-US,zh-CN
2 | header.title,Header Title,标题
3 |
--------------------------------------------------------------------------------
/exchange_actions/public/imports/import_1.csv:
--------------------------------------------------------------------------------
1 | Translation Keys,en-US,zh-CN
2 | header.title,Header Title,标题
3 | qor_admin.description,description,描述
4 | qor_admin.subtitle,subtitle,小标题
5 | qor_admin.title,title,标题
6 |
--------------------------------------------------------------------------------
/exchange_actions/public/imports/import_2.csv:
--------------------------------------------------------------------------------
1 | Translation Keys,en-US,zh-CN
2 | header.title,Header Title,
3 | qor_admin.description,description,描述
4 | qor_admin.subtitle,subtitle,小标题
5 | qor_admin.title,title,标题
6 |
--------------------------------------------------------------------------------
/exchange_actions/public/imports/import_3.csv:
--------------------------------------------------------------------------------
1 | Translation Keys,en-US,zh-CN,,
2 | header.title,Header Title,标题,,
3 | qor_admin.description,description,描述,,
4 | qor_admin.subtitle,subtitle,小标题,,
5 | qor_admin.title,title,标题,,
6 |
--------------------------------------------------------------------------------
/exchange_actions/views/themes/i18n/actions/index/exchange.tmpl:
--------------------------------------------------------------------------------
1 |
2 | {{$worker := get_resource "Worker"}}
3 | {{$prefix := .Admin.GetRouter.Prefix}}
4 |
5 | {{$importTranslationURL := (printf "%v/%v/new?job=Import Translations" $prefix $worker.ToParam)}}
6 |
9 |
10 | {{$exportTranslationURL := (printf "%v/%v/new?job=Export Translations" $prefix $worker.ToParam)}}
11 |
14 |
15 |
--------------------------------------------------------------------------------
/i18n.go:
--------------------------------------------------------------------------------
1 | package i18n
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "html/template"
7 | "io/ioutil"
8 | "net/http"
9 | "path/filepath"
10 | "sort"
11 | "strconv"
12 | "strings"
13 |
14 | "github.com/qor/admin"
15 | "github.com/qor/cache"
16 | "github.com/qor/cache/memory"
17 | "github.com/qor/qor"
18 | "github.com/qor/qor/resource"
19 | "github.com/qor/qor/utils"
20 | "github.com/theplant/cldr"
21 | )
22 |
23 | // Default default locale for i18n
24 | var Default = "en-US"
25 |
26 | // I18n struct that hold all translations
27 | type I18n struct {
28 | Resource *admin.Resource
29 | scope string
30 | value string
31 | Backends []Backend
32 | FallbackLocales map[string][]string
33 | fallbackLocales []string
34 | cacheStore cache.CacheStoreInterface
35 | }
36 |
37 | // ResourceName change display name in qor admin
38 | func (I18n) ResourceName() string {
39 | return "Translation"
40 | }
41 |
42 | // Backend defined methods that needs for translation backend
43 | type Backend interface {
44 | LoadTranslations() []*Translation
45 | SaveTranslation(*Translation) error
46 | FindTranslation(*Translation) Translation
47 | DeleteTranslation(*Translation) error
48 | }
49 |
50 | // Translation is a struct for translations, including Translation Key, Locale, Value
51 | type Translation struct {
52 | Key string
53 | Locale string
54 | Value string
55 | Backend Backend `json:"-"`
56 | }
57 |
58 | // New initialize I18n with backends
59 | func New(backends ...Backend) *I18n {
60 | i18n := &I18n{Backends: backends, cacheStore: memory.New()}
61 | i18n.loadToCacheStore()
62 | return i18n
63 | }
64 |
65 | // SetCacheStore set i18n's cache store
66 | func (i18n *I18n) SetCacheStore(cacheStore cache.CacheStoreInterface) {
67 | i18n.cacheStore = cacheStore
68 | i18n.loadToCacheStore()
69 | }
70 |
71 | func (i18n *I18n) loadToCacheStore() {
72 | backends := i18n.Backends
73 | for i := len(backends) - 1; i >= 0; i-- {
74 | var backend = backends[i]
75 | for _, translation := range backend.LoadTranslations() {
76 | i18n.AddTranslation(translation)
77 | }
78 | }
79 | }
80 |
81 | // LoadTranslations load translations as map `map[locale]map[key]*Translation`
82 | func (i18n *I18n) LoadTranslations() map[string]map[string]*Translation {
83 | var translations = map[string]map[string]*Translation{}
84 |
85 | for i := len(i18n.Backends); i > 0; i-- {
86 | backend := i18n.Backends[i-1]
87 | for _, translation := range backend.LoadTranslations() {
88 | if translations[translation.Locale] == nil {
89 | translations[translation.Locale] = map[string]*Translation{}
90 | }
91 | translations[translation.Locale][translation.Key] = translation
92 | }
93 | }
94 | return translations
95 | }
96 |
97 | // AddTranslation add translation
98 | func (i18n *I18n) AddTranslation(translation *Translation) error {
99 | return i18n.cacheStore.Set(cacheKey(translation.Locale, translation.Key), translation)
100 | }
101 |
102 | // SaveTranslation save translation
103 | func (i18n *I18n) SaveTranslation(translation *Translation) error {
104 | for _, backend := range i18n.Backends {
105 | if backend.SaveTranslation(translation) == nil {
106 | i18n.AddTranslation(translation)
107 | return nil
108 | }
109 | }
110 |
111 | return errors.New("failed to save translation")
112 | }
113 |
114 | // DeleteTranslation delete translation
115 | func (i18n *I18n) DeleteTranslation(translation *Translation) (err error) {
116 | for _, backend := range i18n.Backends {
117 | backend.DeleteTranslation(translation)
118 | }
119 |
120 | return i18n.cacheStore.Delete(cacheKey(translation.Locale, translation.Key))
121 | }
122 |
123 | // Scope i18n scope
124 | func (i18n *I18n) Scope(scope string) admin.I18n {
125 | return &I18n{cacheStore: i18n.cacheStore, scope: scope, value: i18n.value, Backends: i18n.Backends, Resource: i18n.Resource, FallbackLocales: i18n.FallbackLocales, fallbackLocales: i18n.fallbackLocales}
126 | }
127 |
128 | // Default default value of translation if key is missing
129 | func (i18n *I18n) Default(value string) admin.I18n {
130 | return &I18n{cacheStore: i18n.cacheStore, scope: i18n.scope, value: value, Backends: i18n.Backends, Resource: i18n.Resource, FallbackLocales: i18n.FallbackLocales, fallbackLocales: i18n.fallbackLocales}
131 | }
132 |
133 | // Fallbacks fallback to locale if translation doesn't exist in specified locale
134 | func (i18n *I18n) Fallbacks(locale ...string) admin.I18n {
135 | return &I18n{cacheStore: i18n.cacheStore, scope: i18n.scope, value: i18n.value, Backends: i18n.Backends, Resource: i18n.Resource, FallbackLocales: i18n.FallbackLocales, fallbackLocales: locale}
136 | }
137 |
138 | // T translate with locale, key and arguments
139 | func (i18n *I18n) T(locale, key string, args ...interface{}) template.HTML {
140 | var (
141 | value = i18n.value
142 | translationKey = key
143 | fallbackLocales = i18n.fallbackLocales
144 | )
145 |
146 | if locale == "" {
147 | locale = Default
148 | }
149 |
150 | if locales, ok := i18n.FallbackLocales[locale]; ok {
151 | fallbackLocales = append(fallbackLocales, locales...)
152 | }
153 | fallbackLocales = append(fallbackLocales, Default)
154 |
155 | if i18n.scope != "" {
156 | translationKey = strings.Join([]string{i18n.scope, key}, ".")
157 | }
158 |
159 | var translation Translation
160 | if err := i18n.cacheStore.Unmarshal(cacheKey(locale, key), &translation); err != nil || translation.Value == "" {
161 | for _, fallbackLocale := range fallbackLocales {
162 | if err := i18n.cacheStore.Unmarshal(cacheKey(fallbackLocale, key), &translation); err == nil && translation.Value != "" {
163 | break
164 | }
165 | }
166 |
167 | if translation.Value == "" {
168 | // Get default translation if not translated
169 | if err := i18n.cacheStore.Unmarshal(cacheKey(Default, key), &translation); err != nil || translation.Value == "" {
170 | // If not initialized
171 | var defaultBackend Backend
172 | if len(i18n.Backends) > 0 {
173 | defaultBackend = i18n.Backends[0]
174 | }
175 |
176 | translation = Translation{Key: translationKey, Value: value, Locale: locale, Backend: defaultBackend}
177 | if t := defaultBackend.FindTranslation(&translation); t.Value != "" {
178 | translation = t
179 | } else {
180 | i18n.SaveTranslation(&translation)
181 | }
182 | }
183 | }
184 | }
185 |
186 | if translation.Value != "" {
187 | value = translation.Value
188 | } else {
189 | value = key
190 | }
191 |
192 | if str, err := cldr.Parse(locale, value, args...); err == nil {
193 | value = str
194 | }
195 |
196 | return template.HTML(value)
197 | }
198 |
199 | // RenderInlineEditAssets render inline edit html, it is using: http://vitalets.github.io/x-editable/index.html
200 | // You could use Bootstrap or JQuery UI by set isIncludeExtendAssetLib to false and load files by yourself
201 | func RenderInlineEditAssets(isIncludeJQuery bool, isIncludeExtendAssetLib bool) (template.HTML, error) {
202 | for _, gopath := range utils.GOPATH() {
203 | var content string
204 | var hasError bool
205 |
206 | if isIncludeJQuery {
207 | content = ``
208 | }
209 |
210 | if isIncludeExtendAssetLib {
211 | if extendLib, err := ioutil.ReadFile(filepath.Join(gopath, "src/github.com/qor/i18n/views/themes/i18n/inline-edit-libs.tmpl")); err == nil {
212 | content += string(extendLib)
213 | } else {
214 | hasError = true
215 | }
216 |
217 | if css, err := ioutil.ReadFile(filepath.Join(gopath, "src/github.com/qor/i18n/views/themes/i18n/assets/stylesheets/i18n-inline.css")); err == nil {
218 | content += fmt.Sprintf("", string(css))
219 | } else {
220 | hasError = true
221 | }
222 |
223 | }
224 |
225 | if js, err := ioutil.ReadFile(filepath.Join(gopath, "src/github.com/qor/i18n/views/themes/i18n/assets/javascripts/i18n-inline.js")); err == nil {
226 | content += fmt.Sprintf("", string(js))
227 | } else {
228 | hasError = true
229 | }
230 |
231 | if !hasError {
232 | return template.HTML(content), nil
233 | }
234 | }
235 |
236 | return template.HTML(""), errors.New("templates not found")
237 | }
238 |
239 | func getLocaleFromContext(context *qor.Context) string {
240 | if locale := utils.GetLocale(context); locale != "" {
241 | return locale
242 | }
243 |
244 | return Default
245 | }
246 |
247 | type availableLocalesInterface interface {
248 | AvailableLocales() []string
249 | }
250 |
251 | type viewableLocalesInterface interface {
252 | ViewableLocales() []string
253 | }
254 |
255 | type editableLocalesInterface interface {
256 | EditableLocales() []string
257 | }
258 |
259 | func getAvailableLocales(req *http.Request, currentUser qor.CurrentUser) []string {
260 | if user, ok := currentUser.(viewableLocalesInterface); ok {
261 | return user.ViewableLocales()
262 | }
263 |
264 | if user, ok := currentUser.(availableLocalesInterface); ok {
265 | return user.AvailableLocales()
266 | }
267 | return []string{Default}
268 | }
269 |
270 | func getEditableLocales(req *http.Request, currentUser qor.CurrentUser) []string {
271 | if user, ok := currentUser.(editableLocalesInterface); ok {
272 | return user.EditableLocales()
273 | }
274 |
275 | if user, ok := currentUser.(availableLocalesInterface); ok {
276 | return user.AvailableLocales()
277 | }
278 | return []string{Default}
279 | }
280 |
281 | // ConfigureQorResource configure qor resource for qor admin
282 | func (i18n *I18n) ConfigureQorResource(res resource.Resourcer) {
283 | if res, ok := res.(*admin.Resource); ok {
284 | i18n.Resource = res
285 | res.UseTheme("i18n")
286 | res.GetAdmin().I18n = i18n
287 | res.SearchAttrs("value") // generate search handler for i18n
288 |
289 | var getPrimaryLocale = func(context *admin.Context) string {
290 | if locale := context.Request.Form.Get("primary_locale"); locale != "" {
291 | return locale
292 | }
293 | if availableLocales := getAvailableLocales(context.Request, context.CurrentUser); len(availableLocales) > 0 {
294 | return availableLocales[0]
295 | }
296 | return ""
297 | }
298 |
299 | var getEditingLocale = func(context *admin.Context) string {
300 | if locale := context.Request.Form.Get("to_locale"); locale != "" {
301 | return locale
302 | }
303 | return getLocaleFromContext(context.Context)
304 | }
305 |
306 | type matchedTranslation struct {
307 | Key string
308 | PrimaryLocale string
309 | PrimaryValue string
310 | EditingLocale string
311 | EditingValue string
312 | }
313 |
314 | res.GetAdmin().RegisterFuncMap("i18n_available_translations", func(context *admin.Context) (results []matchedTranslation) {
315 | var (
316 | translationsMap = i18n.LoadTranslations()
317 | matchedTranslations = map[string]matchedTranslation{}
318 | keys = []string{}
319 | keyword = strings.ToLower(context.Request.URL.Query().Get("keyword"))
320 | primaryLocale = getPrimaryLocale(context)
321 | editingLocale = getEditingLocale(context)
322 | )
323 |
324 | var filterTranslations = func(translations map[string]*Translation, isPrimary bool) {
325 | if translations != nil {
326 | for key, translation := range translations {
327 | if (keyword == "") || (strings.Index(strings.ToLower(translation.Key), keyword) != -1 ||
328 | strings.Index(strings.ToLower(translation.Value), keyword) != -1) {
329 | if _, ok := matchedTranslations[key]; !ok {
330 | var t = matchedTranslation{
331 | Key: key,
332 | PrimaryLocale: primaryLocale,
333 | EditingLocale: editingLocale,
334 | EditingValue: translation.Value,
335 | }
336 |
337 | if localeTranslations, ok := translationsMap[primaryLocale]; ok {
338 | if v, ok := localeTranslations[key]; ok {
339 | t.PrimaryValue = v.Value
340 | }
341 | }
342 |
343 | matchedTranslations[key] = t
344 | keys = append(keys, key)
345 | }
346 | }
347 | }
348 | }
349 | }
350 |
351 | filterTranslations(translationsMap[getEditingLocale(context)], false)
352 | if primaryLocale != editingLocale {
353 | filterTranslations(translationsMap[getPrimaryLocale(context)], true)
354 | }
355 |
356 | sort.Strings(keys)
357 |
358 | pagination := context.Searcher.Pagination
359 | pagination.Total = len(keys)
360 | pagination.PerPage, _ = strconv.Atoi(context.Request.URL.Query().Get("per_page"))
361 | pagination.CurrentPage, _ = strconv.Atoi(context.Request.URL.Query().Get("page"))
362 |
363 | if pagination.CurrentPage == 0 {
364 | pagination.CurrentPage = 1
365 | }
366 |
367 | if pagination.PerPage == 0 {
368 | pagination.PerPage = 25
369 | }
370 |
371 | if pagination.CurrentPage > 0 {
372 | pagination.Pages = pagination.Total / pagination.PerPage
373 | }
374 |
375 | context.Searcher.Pagination = pagination
376 |
377 | var paginationKeys []string
378 | if pagination.CurrentPage == -1 {
379 | paginationKeys = keys
380 | } else {
381 | lastIndex := pagination.CurrentPage * pagination.PerPage
382 | if pagination.Total < lastIndex {
383 | lastIndex = pagination.Total
384 | }
385 |
386 | startIndex := (pagination.CurrentPage - 1) * pagination.PerPage
387 | if lastIndex >= startIndex {
388 | paginationKeys = keys[startIndex:lastIndex]
389 | }
390 | }
391 |
392 | for _, key := range paginationKeys {
393 | results = append(results, matchedTranslations[key])
394 | }
395 | return results
396 | })
397 |
398 | res.GetAdmin().RegisterFuncMap("i18n_primary_locale", getPrimaryLocale)
399 |
400 | res.GetAdmin().RegisterFuncMap("i18n_editing_locale", getEditingLocale)
401 |
402 | res.GetAdmin().RegisterFuncMap("i18n_viewable_locales", func(context admin.Context) []string {
403 | return getAvailableLocales(context.Request, context.CurrentUser)
404 | })
405 |
406 | res.GetAdmin().RegisterFuncMap("i18n_editable_locales", func(context admin.Context) []string {
407 | return getEditableLocales(context.Request, context.CurrentUser)
408 | })
409 |
410 | controller := i18nController{i18n}
411 | router := res.GetAdmin().GetRouter()
412 | router.Get(res.ToParam(), controller.Index, &admin.RouteConfig{Resource: res})
413 | router.Post(res.ToParam(), controller.Update, &admin.RouteConfig{Resource: res})
414 | router.Put(res.ToParam(), controller.Update, &admin.RouteConfig{Resource: res})
415 |
416 | res.GetAdmin().RegisterViewPath("github.com/qor/i18n/views")
417 | }
418 | }
419 |
420 | func cacheKey(strs ...string) string {
421 | return strings.Join(strs, "/")
422 | }
423 |
--------------------------------------------------------------------------------
/i18n_test.go:
--------------------------------------------------------------------------------
1 | package i18n
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | type backend struct{}
9 |
10 | func (b *backend) LoadTranslations() (translations []*Translation) { return translations }
11 | func (b *backend) SaveTranslation(t *Translation) error { return nil }
12 | func (b *backend) DeleteTranslation(t *Translation) error { return nil }
13 |
14 | const BIGNUM = 10000
15 |
16 | // run TestConcurrent* tests with -race flag would be better
17 |
18 | func TestConcurrentReadWrite(t *testing.T) {
19 | i18n := New(&backend{})
20 | go func() {
21 | for i := 0; i < BIGNUM; i++ {
22 | i18n.AddTranslation(&Translation{Key: fmt.Sprintf("xx-%d", i), Locale: "xx", Value: fmt.Sprint(i)})
23 | }
24 | }()
25 | for i := 0; i < BIGNUM; i++ {
26 | i18n.T("xx", fmt.Sprintf("xx-%d", i))
27 | }
28 | }
29 |
30 | func TestConcurrentDeleteWrite(t *testing.T) {
31 | i18n := New(&backend{})
32 | go func() {
33 | for i := 0; i < BIGNUM; i++ {
34 | i18n.AddTranslation(&Translation{Key: fmt.Sprintf("xx-%d", i), Locale: "xx", Value: fmt.Sprint(i)})
35 | }
36 | }()
37 | for i := 0; i < BIGNUM; i++ {
38 | i18n.DeleteTranslation(&Translation{Key: fmt.Sprintf("xx-%d", i), Locale: "xx", Value: fmt.Sprint(i)})
39 | }
40 | }
41 |
42 | func TestFallbackLocale(t *testing.T) {
43 | i18n := New(&backend{})
44 | i18n.AddTranslation(&Translation{Key: "hello-world", Locale: "en-AU", Value: "Hello World"})
45 |
46 | if i18n.Fallbacks("en-AU").T("en-UK", "hello-world") != "Hello World" {
47 | t.Errorf("Should fallback en-UK to en-US")
48 | }
49 |
50 | if i18n.T("en-DE", "hello-world") != "hello-world" {
51 | t.Errorf("Haven't setup any fallback")
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/inline_edit/inline_edit.go:
--------------------------------------------------------------------------------
1 | package inline_edit
2 |
3 | import (
4 | "fmt"
5 | "html/template"
6 |
7 | "github.com/qor/admin"
8 | "github.com/qor/i18n"
9 | )
10 |
11 | func init() {
12 | admin.RegisterViewPath("github.com/qor/i18n/inline_edit/views")
13 | }
14 |
15 | // FuncMap generate func map for inline edit
16 | func FuncMap(I18n *i18n.I18n, locale string, enableInlineEdit bool) template.FuncMap {
17 | return template.FuncMap{
18 | "t": InlineEdit(I18n, locale, enableInlineEdit),
19 | }
20 | }
21 |
22 | // InlineEdit enable inline edit
23 | func InlineEdit(I18n *i18n.I18n, locale string, isInline bool) func(string, ...interface{}) template.HTML {
24 | return func(key string, args ...interface{}) template.HTML {
25 | // Get Translation Value
26 | var value template.HTML
27 | var defaultValue string
28 | if len(args) > 0 {
29 | if args[0] == nil {
30 | defaultValue = key
31 | } else {
32 | defaultValue = fmt.Sprint(args[0])
33 | }
34 | value = I18n.Default(defaultValue).T(locale, key, args[1:]...)
35 | } else {
36 | value = I18n.T(locale, key)
37 | }
38 |
39 | // Append inline-edit script/tag
40 | if isInline {
41 | var editType string
42 | if len(value) > 25 {
43 | editType = "data-type=\"textarea\""
44 | }
45 | prefix := I18n.Resource.GetAdmin().GetRouter().Prefix
46 | assetsTag := fmt.Sprintf("", prefix, prefix)
47 | return template.HTML(fmt.Sprintf("%s%s", assetsTag, editType, locale, key, string(value)))
48 | }
49 | return value
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/inline_edit/views/themes/i18n/assets/javascripts/i18n-checker.js:
--------------------------------------------------------------------------------
1 | if (!window.loadedI18nAsset) {
2 | window.loadjscssfile = function (filename, filetype) {
3 | var fileref;
4 | if (filetype == "js"){
5 | fileref = document.createElement('script');
6 | fileref.setAttribute("type", "text/javascript");
7 | fileref.setAttribute("src", filename);
8 | } else if (filetype == "css"){
9 | fileref = document.createElement("link");
10 | fileref.setAttribute("rel", "stylesheet");
11 | fileref.setAttribute("type", "text/css");
12 | fileref.setAttribute("href", filename);
13 | }
14 | if (typeof fileref != "undefined")
15 | document.getElementsByTagName("head")[0].appendChild(fileref);
16 | };
17 |
18 | window.loadedI18nAsset = true;
19 | var prefix = document.currentScript.getAttribute("data-prefix");
20 | window.jQuery || loadjscssfile(prefix + "/assets/javascripts/vendors/jquery.min.js", "js");
21 | loadjscssfile(prefix + "/assets/javascripts/i18n-inline.js?theme=i18n", "js");
22 | loadjscssfile(prefix + "/assets/stylesheets/i18n-inline.css?theme=i18n", "css");
23 | }
24 |
--------------------------------------------------------------------------------
/inline_edit/views/themes/i18n/assets/javascripts/i18n-inline.js:
--------------------------------------------------------------------------------
1 | !function(t){function i(){t.each(e,function(){this.refresh(!0)})}var e=[],s=/^url\(["']?([^"'\)]*)["']?\);?$/i,n=/\.png$/i,o=!!window.createPopup&&"undefined"==document.documentElement.currentStyle.minWidth;t(window).resize(i),t.Poshytip=function(i,e){this.$elm=t(i),this.opts=t.extend({},t.fn.poshytip.defaults,e),this.$tip=t(['"].join("")).appendTo(document.body),this.$arrow=this.$tip.find("div.tip-arrow"),this.$inner=this.$tip.find("div.tip-inner"),this.disabled=!1,this.content=null,this.init()},t.Poshytip.prototype={init:function(){e.push(this);var i=this.$elm.attr("title");if(this.$elm.data("title.poshytip",void 0!==i?i:null).data("poshytip",this),"none"!=this.opts.showOn)switch(this.$elm.bind({"mouseenter.poshytip":t.proxy(this.mouseenter,this),"mouseleave.poshytip":t.proxy(this.mouseleave,this)}),this.opts.showOn){case"hover":"cursor"==this.opts.alignTo&&this.$elm.bind("mousemove.poshytip",t.proxy(this.mousemove,this)),this.opts.allowTipHover&&this.$tip.hover(t.proxy(this.clearTimeouts,this),t.proxy(this.mouseleave,this));break;case"focus":this.$elm.bind({"focus.poshytip":t.proxy(this.showDelayed,this),"blur.poshytip":t.proxy(this.hideDelayed,this)})}},mouseenter:function(t){return this.disabled?!0:(this.$elm.attr("title",""),"focus"==this.opts.showOn?!0:void this.showDelayed())},mouseleave:function(t){if(this.disabled||this.asyncAnimating&&(this.$tip[0]===t.relatedTarget||jQuery.contains(this.$tip[0],t.relatedTarget)))return!0;if(!this.$tip.data("active")){var i=this.$elm.data("title.poshytip");null!==i&&this.$elm.attr("title",i)}return"focus"==this.opts.showOn?!0:void this.hideDelayed()},mousemove:function(t){return this.disabled?!0:(this.eventX=t.pageX,this.eventY=t.pageY,void(this.opts.followCursor&&this.$tip.data("active")&&(this.calcPos(),this.$tip.css({left:this.pos.l,top:this.pos.t}),this.pos.arrow&&(this.$arrow[0].className="tip-arrow tip-arrow-"+this.pos.arrow))))},show:function(){this.disabled||this.$tip.data("active")||(this.reset(),this.update(),this.content&&(this.display(),this.opts.timeOnScreen&&this.hideDelayed(this.opts.timeOnScreen)))},showDelayed:function(i){this.clearTimeouts(),this.showTimeout=setTimeout(t.proxy(this.show,this),"number"==typeof i?i:this.opts.showTimeout)},hide:function(){!this.disabled&&this.$tip.data("active")&&this.display(!0)},hideDelayed:function(i){this.clearTimeouts(),this.hideTimeout=setTimeout(t.proxy(this.hide,this),"number"==typeof i?i:this.opts.hideTimeout)},reset:function(){this.$tip.queue([]).detach().css("visibility","hidden").data("active",!1),this.$inner.find("*").poshytip("hide"),this.opts.fade&&this.$tip.css("opacity",this.opacity),this.$arrow[0].className="tip-arrow tip-arrow-top tip-arrow-right tip-arrow-bottom tip-arrow-left",this.asyncAnimating=!1},update:function(t,i){if(!this.disabled){var e=void 0!==t;if(e){if(i||(this.opts.content=t),!this.$tip.data("active"))return}else t=this.opts.content;var s=this,n="function"==typeof t?t.call(this.$elm[0],function(t){s.update(t)}):"[title]"==t?this.$elm.data("title.poshytip"):t;this.content!==n&&(this.$inner.empty().append(n),this.content=n),this.refresh(e)}},refresh:function(i){if(!this.disabled){if(i){if(!this.$tip.data("active"))return;var e={left:this.$tip.css("left"),top:this.$tip.css("top")}}this.$tip.css({left:0,top:0}).appendTo(document.body),void 0===this.opacity&&(this.opacity=this.$tip.css("opacity"));var a=this.$tip.css("background-image").match(s),r=this.$arrow.css("background-image").match(s);if(a){var l=n.test(a[1]);o&&l?(this.$tip.css("background-image","none"),this.$inner.css({margin:0,border:0,padding:0}),a=l=!1):this.$tip.prepend('').css({border:0,padding:0,"background-image":"none","background-color":"transparent"}).find(".tip-bg-image").css("background-image",'url("'+a[1]+'")').end().find("td").eq(3).append(this.$inner),l&&!t.support.opacity&&(this.opts.fade=!1)}r&&!t.support.opacity&&(o&&n.test(r[1])&&(r=!1,this.$arrow.css("background-image","none")),this.opts.fade=!1);var h=this.$tip.find("> table.tip-table");if(o){this.$tip[0].style.width="",h.width("auto").find("td").eq(3).width("auto");var p=this.$tip.width(),u=parseInt(this.$tip.css("min-width")),d=parseInt(this.$tip.css("max-width"));!isNaN(u)&&u>p?p=u:!isNaN(d)&&p>d&&(p=d),this.$tip.add(h).width(p).eq(0).find("td").eq(3).width("100%")}else h[0]&&h.width("auto").find("td").eq(3).width("auto").end().end().width(document.defaultView&&document.defaultView.getComputedStyle&&parseFloat(document.defaultView.getComputedStyle(this.$tip[0],null).width)||this.$tip.width()).find("td").eq(3).width("100%");if(this.tipOuterW=this.$tip.outerWidth(),this.tipOuterH=this.$tip.outerHeight(),this.calcPos(),r&&this.pos.arrow&&(this.$arrow[0].className="tip-arrow tip-arrow-"+this.pos.arrow,this.$arrow.css("visibility","inherit")),i&&this.opts.refreshAniDuration){this.asyncAnimating=!0;var c=this;this.$tip.css(e).animate({left:this.pos.l,top:this.pos.t},this.opts.refreshAniDuration,function(){c.asyncAnimating=!1})}else this.$tip.css({left:this.pos.l,top:this.pos.t})}},display:function(i){var e=this.$tip.data("active");if(!(e&&!i||!e&&i)){if(this.$tip.stop(),(this.opts.slide&&this.pos.arrow||this.opts.fade)&&(i&&this.opts.hideAniDuration||!i&&this.opts.showAniDuration)){var s={},n={};if(this.opts.slide&&this.pos.arrow){var o,a;"bottom"==this.pos.arrow||"top"==this.pos.arrow?(o="top",a="bottom"):(o="left",a="right");var r=parseInt(this.$tip.css(o));s[o]=r+(i?0:this.pos.arrow==a?-this.opts.slideOffset:this.opts.slideOffset),n[o]=r+(i?this.pos.arrow==a?this.opts.slideOffset:-this.opts.slideOffset:0)+"px"}this.opts.fade&&(s.opacity=i?this.$tip.css("opacity"):0,n.opacity=i?0:this.opacity),this.$tip.css(s).animate(n,this.opts[i?"hideAniDuration":"showAniDuration"])}if(i?this.$tip.queue(t.proxy(this.reset,this)):this.$tip.css("visibility","inherit"),e){var l=this.$elm.data("title.poshytip");null!==l&&this.$elm.attr("title",l)}this.$tip.data("active",!e)}},disable:function(){this.reset(),this.disabled=!0},enable:function(){this.disabled=!1},destroy:function(){this.reset(),this.$tip.remove(),delete this.$tip,this.content=null,this.$elm.unbind(".poshytip").removeData("title.poshytip").removeData("poshytip"),e.splice(t.inArray(this,e),1)},clearTimeouts:function(){this.showTimeout&&(clearTimeout(this.showTimeout),this.showTimeout=0),this.hideTimeout&&(clearTimeout(this.hideTimeout),this.hideTimeout=0)},calcPos:function(){var i,e,s,n,o,a,r={l:0,t:0,arrow:""},l=t(window),h={l:l.scrollLeft(),t:l.scrollTop(),w:l.width(),h:l.height()};if("cursor"==this.opts.alignTo)i=e=s=this.eventX,n=o=a=this.eventY;else{var p=this.$elm.offset(),u={l:p.left,t:p.top,w:this.$elm.outerWidth(),h:this.$elm.outerHeight()};i=u.l+("inner-right"!=this.opts.alignX?0:u.w),e=i+Math.floor(u.w/2),s=i+("inner-left"!=this.opts.alignX?u.w:0),n=u.t+("inner-bottom"!=this.opts.alignY?0:u.h),o=n+Math.floor(u.h/2),a=n+("inner-top"!=this.opts.alignY?u.h:0)}switch(this.opts.alignX){case"right":case"inner-left":r.l=s+this.opts.offsetX,this.opts.keepInViewport&&r.l+this.tipOuterW>h.l+h.w&&(r.l=h.l+h.w-this.tipOuterW),"right"!=this.opts.alignX&&"center"!=this.opts.alignY||(r.arrow="left");break;case"center":r.l=e-Math.floor(this.tipOuterW/2),this.opts.keepInViewport&&(r.l+this.tipOuterW>h.l+h.w?r.l=h.l+h.w-this.tipOuterW:r.lh.t+h.h&&(r.t=n-this.tipOuterH-this.opts.offsetY,"top"==r.arrow&&(r.arrow="bottom"));break;case"center":r.t=o-Math.floor(this.tipOuterH/2),this.opts.keepInViewport&&(r.t+this.tipOuterH>h.t+h.h?r.t=h.t+h.h-this.tipOuterH:r.t',"div.",n.className,"{visibility:hidden;position:absolute;top:0;left:0;}","div.",n.className," table.tip-table, div.",n.className," table.tip-table td{margin:0;font-family:inherit;font-size:inherit;font-weight:inherit;font-style:inherit;font-variant:inherit;vertical-align:middle;}","div.",n.className," td.tip-bg-image span{display:block;font:1px/1px sans-serif;height:",n.bgImageFrameSize,"px;width:",n.bgImageFrameSize,"px;overflow:hidden;}","div.",n.className," td.tip-right{background-position:100% 0;}","div.",n.className," td.tip-bottom{background-position:100% 100%;}","div.",n.className," td.tip-left{background-position:0 100%;}","div.",n.className," div.tip-inner{background-position:-",n.bgImageFrameSize,"px -",n.bgImageFrameSize,"px;}","div.",n.className," div.tip-arrow{visibility:hidden;position:absolute;overflow:hidden;font:1px/1px sans-serif;}",""].join("")).appendTo("head"),n.liveEvents&&"none"!=n.showOn){var o,a=t.extend({},n,{liveEvents:!1});switch(n.showOn){case"hover":o=function(){var i=t(this);i.data("poshytip")||i.poshytip(a).poshytip("mouseenter")},this.live?this.live("mouseenter.poshytip",o):t(document).delegate(this.selector,"mouseenter.poshytip",o);break;case"focus":o=function(){var i=t(this);i.data("poshytip")||i.poshytip(a).poshytip("showDelayed")},this.live?this.live("focus.poshytip",o):t(document).delegate(this.selector,"focus.poshytip",o)}return this}return this.each(function(){new t.Poshytip(this,n)})},t.fn.poshytip.defaults={content:"[title]",className:"tip-yellow",bgImageFrameSize:10,showTimeout:500,hideTimeout:100,timeOnScreen:0,showOn:"hover",liveEvents:!1,alignTo:"cursor",alignX:"right",alignY:"top",offsetX:-22,offsetY:18,keepInViewport:!0,allowTipHover:!0,followCursor:!1,fade:!0,slide:!0,slideOffset:8,showAniDuration:300,hideAniDuration:300,refreshAniDuration:200}}(jQuery),function(t){"use strict";var i=function(i,e){this.options=t.extend({},t.fn.editableform.defaults,e),this.$div=t(i),this.options.scope||(this.options.scope=this)};i.prototype={constructor:i,initInput:function(){this.input=this.options.input,this.value=this.input.str2value(this.options.value),this.input.prerender()},initTemplate:function(){this.$form=t(t.fn.editableform.template)},initButtons:function(){var i=this.$form.find(".editable-buttons");i.append(t.fn.editableform.buttons),"bottom"===this.options.showbuttons&&i.addClass("editable-buttons-bottom")},render:function(){this.$loading=t(t.fn.editableform.loading),this.$div.empty().append(this.$loading),this.initTemplate(),this.options.showbuttons?this.initButtons():this.$form.find(".editable-buttons").remove(),this.showLoading(),this.isSaving=!1,this.$div.triggerHandler("rendering"),this.initInput(),this.$form.find("div.editable-input").append(this.input.$tpl),this.$div.append(this.$form),t.when(this.input.render()).then(t.proxy(function(){if(this.options.showbuttons||this.input.autosubmit(),this.$form.find(".editable-cancel").click(t.proxy(this.cancel,this)),this.input.error)this.error(this.input.error),this.$form.find(".editable-submit").attr("disabled",!0),this.input.$input.attr("disabled",!0),this.$form.submit(function(t){t.preventDefault()});else{this.error(!1),this.input.$input.removeAttr("disabled"),this.$form.find(".editable-submit").removeAttr("disabled");var i=null===this.value||void 0===this.value||""===this.value?this.options.defaultValue:this.value;this.input.value2input(i),this.$form.submit(t.proxy(this.submit,this))}this.$div.triggerHandler("rendered"),this.showForm(),this.input.postrender&&this.input.postrender()},this))},cancel:function(){this.$div.triggerHandler("cancel")},showLoading:function(){var t,i;this.$form?(t=this.$form.outerWidth(),i=this.$form.outerHeight(),t&&this.$loading.width(t),i&&this.$loading.height(i),this.$form.hide()):(t=this.$loading.parent().width(),t&&this.$loading.width(t)),this.$loading.show()},showForm:function(t){this.$loading.hide(),this.$form.show(),t!==!1&&this.input.activate(),this.$div.triggerHandler("show")},error:function(i){var e,s=this.$form.find(".control-group"),n=this.$form.find(".editable-error-block");if(i===!1)s.removeClass(t.fn.editableform.errorGroupClass),n.removeClass(t.fn.editableform.errorBlockClass).empty().hide();else{if(i){e=i.split("\n");for(var o=0;o").text(e[o]).html();i=e.join("
")}s.addClass(t.fn.editableform.errorGroupClass),n.addClass(t.fn.editableform.errorBlockClass).html(i).show()}},submit:function(i){i.stopPropagation(),i.preventDefault();var e,s=this.input.input2value();if(e=this.validate(s))return this.error(e),void this.showForm();if(!this.options.savenochange&&this.input.value2str(s)==this.input.value2str(this.value))return void this.$div.triggerHandler("nochange");var n=this.input.value2submit(s);this.isSaving=!0,t.when(this.save(n)).done(t.proxy(function(t){this.isSaving=!1;var i="function"==typeof this.options.success?this.options.success.call(this.options.scope,t,s):null;return i===!1?(this.error(!1),void this.showForm(!1)):"string"==typeof i?(this.error(i),void this.showForm()):(i&&"object"==typeof i&&i.hasOwnProperty("newValue")&&(s=i.newValue),this.error(!1),this.value=s,void this.$div.triggerHandler("save",{newValue:s,submitValue:n,response:t}))},this)).fail(t.proxy(function(t){this.isSaving=!1;var i;i="function"==typeof this.options.error?this.options.error.call(this.options.scope,t,s):"string"==typeof t?t:t.responseText||t.statusText||"Unknown error!",this.error(i),this.showForm()},this))},save:function(i){this.options.pk=t.fn.editableutils.tryParseJson(this.options.pk,!0);var e,s="function"==typeof this.options.pk?this.options.pk.call(this.options.scope):this.options.pk,n=!!("function"==typeof this.options.url||this.options.url&&("always"===this.options.send||"auto"===this.options.send&&null!==s&&void 0!==s));return n?(this.showLoading(),e={name:this.options.name||"",value:i,pk:s},"function"==typeof this.options.params?e=this.options.params.call(this.options.scope,e):(this.options.params=t.fn.editableutils.tryParseJson(this.options.params,!0),t.extend(e,this.options.params)),"function"==typeof this.options.url?this.options.url.call(this.options.scope,e):t.ajax(t.extend({url:this.options.url,data:e,type:"POST"},this.options.ajaxOptions))):void 0},validate:function(t){return void 0===t&&(t=this.value),"function"==typeof this.options.validate?this.options.validate.call(this.options.scope,t):void 0},option:function(t,i){t in this.options&&(this.options[t]=i),"value"===t&&this.setValue(i)},setValue:function(t,i){i?this.value=this.input.str2value(t):this.value=t,this.$form&&this.$form.is(":visible")&&this.input.value2input(this.value)}},t.fn.editableform=function(e){var s=arguments;return this.each(function(){var n=t(this),o=n.data("editableform"),a="object"==typeof e&&e;o||n.data("editableform",o=new i(this,a)),"string"==typeof e&&o[e].apply(o,Array.prototype.slice.call(s,1))})},t.fn.editableform.Constructor=i,t.fn.editableform.defaults={type:"text",url:null,params:null,name:null,pk:null,value:null,defaultValue:null,send:"auto",validate:null,success:null,error:null,ajaxOptions:null,showbuttons:!0,scope:null,savenochange:!1},t.fn.editableform.template='',t.fn.editableform.loading='',t.fn.editableform.buttons='',t.fn.editableform.errorGroupClass=null,t.fn.editableform.errorBlockClass="editable-error",t.fn.editableform.engine="jquery"}(window.jQuery),function(t){"use strict";t.fn.editableutils={inherit:function(t,i){var e=function(){};e.prototype=i.prototype,t.prototype=new e,t.prototype.constructor=t,t.superclass=i.prototype},setCursorPosition:function(t,i){if(t.setSelectionRange)t.setSelectionRange(i,i);else if(t.createTextRange){var e=t.createTextRange();e.collapse(!0),e.moveEnd("character",i),e.moveStart("character",i),e.select()}},tryParseJson:function(t,i){if("string"==typeof t&&t.length&&t.match(/^[\{\[].*[\}\]]$/))if(i)try{t=new Function("return "+t)()}catch(e){}finally{return t}else t=new Function("return "+t)();return t},sliceObj:function(i,e,s){var n,o,a={};if(!t.isArray(e)||!e.length)return a;for(var r=0;r").text(i).html()},itemsByValue:function(i,e,s){if(!e||null===i)return[];if("function"!=typeof s){var n=s||"value";s=function(t){return t[n]}}var o=t.isArray(i),a=[],r=this;return t.each(e,function(e,n){if(n.children)a=a.concat(r.itemsByValue(i,n.children,s));else if(o)t.grep(i,function(t){return t==(n&&"object"==typeof n?s(n):n)}).length&&a.push(n);else{var l=n&&"object"==typeof n?s(n):n;i==l&&a.push(n)}}),a},createInput:function(i){var e,s,n,o=i.type;return"date"===o&&("inline"===i.mode?t.fn.editabletypes.datefield?o="datefield":t.fn.editabletypes.dateuifield&&(o="dateuifield"):t.fn.editabletypes.date?o="date":t.fn.editabletypes.dateui&&(o="dateui"),"date"!==o||t.fn.editabletypes.date||(o="combodate")),"datetime"===o&&"inline"===i.mode&&(o="datetimefield"),"wysihtml5"!==o||t.fn.editabletypes[o]||(o="textarea"),"function"==typeof t.fn.editabletypes[o]?(e=t.fn.editabletypes[o],s=this.sliceObj(i,this.objectKeys(e.defaults)),n=new e(s)):(t.error("Unknown type: "+o),!1)},supportsTransitions:function(){var t=document.body||document.documentElement,i=t.style,e="transition",s=["Moz","Webkit","Khtml","O","ms"];if("string"==typeof i[e])return!0;e=e.charAt(0).toUpperCase()+e.substr(1);for(var n=0;n"),this.tip().is(this.innerCss)?this.tip().append(this.$form):this.tip().find(this.innerCss).append(this.$form),this.renderForm()},hide:function(t){if(this.tip()&&this.tip().is(":visible")&&this.$element.hasClass("editable-open")){if(this.$form.data("editableform").isSaving)return void(this.delayedHide={reason:t});this.delayedHide=!1,this.$element.removeClass("editable-open"),this.innerHide(),this.$element.triggerHandler("hidden",t||"manual")}},innerShow:function(){},innerHide:function(){},toggle:function(t){this.container()&&this.tip()&&this.tip().is(":visible")?this.hide():this.show(t)},setPosition:function(){},save:function(t,i){this.$element.triggerHandler("save",i),this.hide("save")},option:function(t,i){this.options[t]=i,t in this.containerOptions?(this.containerOptions[t]=i,this.setContainerOption(t,i)):(this.formOptions[t]=i,this.$form&&this.$form.editableform("option",t,i))},setContainerOption:function(t,i){this.call("option",t,i)},destroy:function(){this.hide(),this.innerDestroy(),this.$element.off("destroyed"),this.$element.removeData("editableContainer")},innerDestroy:function(){},closeOthers:function(i){t(".editable-open").each(function(e,s){if(s!==i&&!t(s).find(i).length){var n=t(s),o=n.data("editableContainer");o&&("cancel"===o.options.onblur?n.data("editableContainer").hide("onblur"):"submit"===o.options.onblur&&n.data("editableContainer").tip().find("form").submit())}})},activate:function(){this.tip&&this.tip().is(":visible")&&this.$form&&this.$form.data("editableform").input.activate()}},t.fn.editableContainer=function(s){var n=arguments;return this.each(function(){var o=t(this),a="editableContainer",r=o.data(a),l="object"==typeof s&&s,h="inline"===l.mode?e:i;r||o.data(a,r=new h(this,l)),"string"==typeof s&&r[s].apply(r,Array.prototype.slice.call(n,1))})},t.fn.editableContainer.Popup=i,t.fn.editableContainer.Inline=e,t.fn.editableContainer.defaults={value:null,placement:"top",autohide:!0,onblur:"cancel",anim:!1,mode:"popup"},jQuery.event.special.destroyed={remove:function(t){t.handler&&t.handler()}}}(window.jQuery),function(t){"use strict";t.extend(t.fn.editableContainer.Inline.prototype,t.fn.editableContainer.Popup.prototype,{containerName:"editableform",innerCss:".editable-inline",containerClass:"editable-container editable-inline",initContainer:function(){this.$tip=t(""),this.options.anim||(this.options.anim=0)},splitOptions:function(){this.containerOptions={},this.formOptions=this.options},tip:function(){return this.$tip},innerShow:function(){this.$element.hide(),this.tip().insertAfter(this.$element).show()},innerHide:function(){this.$tip.hide(this.options.anim,t.proxy(function(){this.$element.show(),this.innerDestroy()},this))},innerDestroy:function(){this.tip()&&this.tip().empty().remove()}})}(window.jQuery),function(t){"use strict";var i=function(i,e){this.$element=t(i),this.options=t.extend({},t.fn.editable.defaults,e,t.fn.editableutils.getConfigData(this.$element)),this.options.selector?this.initLive():this.init(),this.options.highlight&&!t.fn.editableutils.supportsTransitions()&&(this.options.highlight=!1)};i.prototype={constructor:i,init:function(){var i,e=!1;if(this.options.name=this.options.name||this.$element.attr("id"),this.options.scope=this.$element[0],this.input=t.fn.editableutils.createInput(this.options),this.input){switch(void 0===this.options.value||null===this.options.value?(this.value=t.trim(this.$element.html()),e=!0):(this.options.value=t.fn.editableutils.tryParseJson(this.options.value,!0),"string"==typeof this.options.value?this.value=this.input.str2value(this.options.value):this.value=this.options.value),this.$element.addClass("editable"),"textarea"===this.input.type&&this.$element.addClass("editable-pre-wrapped"),"manual"!==this.options.toggle?(this.$element.addClass("editable-click"),this.$element.on(this.options.toggle+".editable",t.proxy(function(t){if(this.options.disabled||t.preventDefault(),"mouseenter"===this.options.toggle)this.show();else{var i="click"!==this.options.toggle;this.toggle(i)}},this))):this.$element.attr("tabindex",-1),"function"==typeof this.options.display&&(this.options.autotext="always"),this.options.autotext){case"always":i=!0;break;case"auto":i=!t.trim(this.$element.text()).length&&null!==this.value&&void 0!==this.value&&!e;break;default:i=!1}t.when(i?this.render():!0).then(t.proxy(function(){this.options.disabled?this.disable():this.enable(),this.$element.triggerHandler("init",this)},this))}},initLive:function(){var i=this.options.selector;this.options.selector=!1,this.options.autotext="never",this.$element.on(this.options.toggle+".editable",i,t.proxy(function(i){var e=t(i.target);e.data("editable")||(e.hasClass(this.options.emptyclass)&&e.empty(),e.editable(this.options).trigger(i))},this))},render:function(t){return this.options.display!==!1?this.input.value2htmlFinal?this.input.value2html(this.value,this.$element[0],this.options.display,t):"function"==typeof this.options.display?this.options.display.call(this.$element[0],this.value,t):this.input.value2html(this.value,this.$element[0]):void 0},enable:function(){this.options.disabled=!1,this.$element.removeClass("editable-disabled"),this.handleEmpty(this.isEmpty),"manual"!==this.options.toggle&&"-1"===this.$element.attr("tabindex")&&this.$element.removeAttr("tabindex")},disable:function(){this.options.disabled=!0,this.hide(),this.$element.addClass("editable-disabled"),this.handleEmpty(this.isEmpty),this.$element.attr("tabindex",-1)},toggleDisabled:function(){this.options.disabled?this.enable():this.disable()},option:function(i,e){return i&&"object"==typeof i?void t.each(i,t.proxy(function(i,e){this.option(t.trim(i),e)},this)):(this.options[i]=e,"disabled"===i?e?this.disable():this.enable():("value"===i&&this.setValue(e),this.container&&this.container.option(i,e),void(this.input.option&&this.input.option(i,e))))},handleEmpty:function(i){this.options.display!==!1&&(void 0!==i?this.isEmpty=i:"function"==typeof this.input.isEmpty?this.isEmpty=this.input.isEmpty(this.$element):this.isEmpty=""===t.trim(this.$element.html()),this.options.disabled?this.isEmpty&&(this.$element.empty(),this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)):this.isEmpty?(this.$element.html(this.options.emptytext),this.options.emptyclass&&this.$element.addClass(this.options.emptyclass)):this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass))},show:function(i){if(!this.options.disabled){if(this.container){if(this.container.tip().is(":visible"))return}else{var e=t.extend({},this.options,{value:this.value,input:this.input});this.$element.editableContainer(e),this.$element.on("save.internal",t.proxy(this.save,this)),this.container=this.$element.data("editableContainer")}this.container.show(i)}},hide:function(){this.container&&this.container.hide()},toggle:function(t){this.container&&this.container.tip().is(":visible")?this.hide():this.show(t)},save:function(t,i){if(this.options.unsavedclass){var e=!1;e=e||"function"==typeof this.options.url,e=e||this.options.display===!1,e=e||void 0!==i.response,e=e||this.options.savenochange&&this.input.value2str(this.value)!==this.input.value2str(i.newValue),e?this.$element.removeClass(this.options.unsavedclass):this.$element.addClass(this.options.unsavedclass)}if(this.options.highlight){var s=this.$element,n=s.css("background-color");s.css("background-color",this.options.highlight),setTimeout(function(){"transparent"===n&&(n=""),s.css("background-color",n),s.addClass("editable-bg-transition"),setTimeout(function(){s.removeClass("editable-bg-transition")},1700)},10)}this.setValue(i.newValue,!1,i.response)},validate:function(){return"function"==typeof this.options.validate?this.options.validate.call(this,this.value):void 0},setValue:function(i,e,s){e?this.value=this.input.str2value(i):this.value=i,this.container&&this.container.option("value",this.value),t.when(this.render(s)).then(t.proxy(function(){this.handleEmpty()},this))},activate:function(){this.container&&this.container.activate()},destroy:function(){this.disable(),this.container&&this.container.destroy(),this.input.destroy(),"manual"!==this.options.toggle&&(this.$element.removeClass("editable-click"),this.$element.off(this.options.toggle+".editable")),this.$element.off("save.internal"),this.$element.removeClass("editable editable-open editable-disabled"),this.$element.removeData("editable")}},t.fn.editable=function(e){var s={},n=arguments,o="editable";switch(e){case"validate":return this.each(function(){var i,e=t(this),n=e.data(o);n&&(i=n.validate())&&(s[n.options.name]=i)}),s;case"getValue":return 2===arguments.length&&arguments[1]===!0?s=this.eq(0).data(o).value:this.each(function(){var i=t(this),e=i.data(o);e&&void 0!==e.value&&null!==e.value&&(s[e.options.name]=e.input.value2submit(e.value))}),s;case"submit":var a,r=arguments[1]||{},l=this,h=this.editable("validate");return t.isEmptyObject(h)?(a=this.editable("getValue"),r.data&&t.extend(a,r.data),t.ajax(t.extend({url:r.url,data:a,type:"POST"},r.ajaxOptions)).success(function(t){"function"==typeof r.success&&r.success.call(l,t,r)}).error(function(){"function"==typeof r.error&&r.error.apply(l,arguments)})):"function"==typeof r.error&&r.error.call(l,h),this}return this.each(function(){var s=t(this),a=s.data(o),r="object"==typeof e&&e;return r&&r.selector?void(a=new i(this,r)):(a||s.data(o,a=new i(this,r)),void("string"==typeof e&&a[e].apply(a,Array.prototype.slice.call(n,1))))})},t.fn.editable.defaults={type:"text",disabled:!1,toggle:"click",emptytext:"Empty",autotext:"auto",value:null,display:null,emptyclass:"editable-empty",unsavedclass:"editable-unsaved",selector:null,highlight:"#FFFF80"}}(window.jQuery),function(t){"use strict";t.fn.editabletypes={};var i=function(){};i.prototype={init:function(i,e,s){this.type=i,this.options=t.extend({},s,e)},prerender:function(){this.$tpl=t(this.options.tpl),this.$input=this.$tpl,this.$clear=null,this.error=null},render:function(){},value2html:function(i,e){t(e)[this.options.escape?"text":"html"](t.trim(i))},html2value:function(i){return t("").html(i).text();
2 | },value2str:function(t){return t},str2value:function(t){return t},value2submit:function(t){return t},value2input:function(t){this.$input.val(t)},input2value:function(){return this.$input.val()},activate:function(){this.$input.is(":visible")&&this.$input.focus()},clear:function(){this.$input.val(null)},escape:function(i){return t("
").text(i).html()},autosubmit:function(){},destroy:function(){},setClass:function(){this.options.inputclass&&this.$input.addClass(this.options.inputclass)},setAttr:function(t){void 0!==this.options[t]&&null!==this.options[t]&&this.$input.attr(t,this.options[t])},option:function(t,i){this.options[t]=i}},i.defaults={tpl:"",inputclass:null,escape:!0,scope:null,showbuttons:!0},t.extend(t.fn.editabletypes,{abstractinput:i})}(window.jQuery),function(t){"use strict";var i=function(t){};t.fn.editableutils.inherit(i,t.fn.editabletypes.abstractinput),t.extend(i.prototype,{render:function(){var i=t.Deferred();return this.error=null,this.onSourceReady(function(){this.renderList(),i.resolve()},function(){this.error=this.options.sourceError,i.resolve()}),i.promise()},html2value:function(t){return null},value2html:function(i,e,s,n){var o=t.Deferred(),a=function(){"function"==typeof s?s.call(e,i,this.sourceData,n):this.value2htmlFinal(i,e),o.resolve()};return null===i?a.call(this):this.onSourceReady(a,function(){o.resolve()}),o.promise()},onSourceReady:function(i,e){var s;if(t.isFunction(this.options.source)?(s=this.options.source.call(this.options.scope),this.sourceData=null):s=this.options.source,this.options.sourceCache&&t.isArray(this.sourceData))return void i.call(this);try{s=t.fn.editableutils.tryParseJson(s,!1)}catch(n){return void e.call(this)}if("string"==typeof s){if(this.options.sourceCache){var o,a=s;if(t(document).data(a)||t(document).data(a,{}),o=t(document).data(a),o.loading===!1&&o.sourceData)return this.sourceData=o.sourceData,this.doPrepend(),void i.call(this);if(o.loading===!0)return o.callbacks.push(t.proxy(function(){this.sourceData=o.sourceData,this.doPrepend(),i.call(this)},this)),void o.err_callbacks.push(t.proxy(e,this));o.loading=!0,o.callbacks=[],o.err_callbacks=[]}var r=t.extend({url:s,type:"get",cache:!1,dataType:"json",success:t.proxy(function(s){o&&(o.loading=!1),this.sourceData=this.makeArray(s),t.isArray(this.sourceData)?(o&&(o.sourceData=this.sourceData,t.each(o.callbacks,function(){this.call()})),this.doPrepend(),i.call(this)):(e.call(this),o&&t.each(o.err_callbacks,function(){this.call()}))},this),error:t.proxy(function(){e.call(this),o&&(o.loading=!1,t.each(o.err_callbacks,function(){this.call()}))},this)},this.options.sourceOptions);t.ajax(r)}else this.sourceData=this.makeArray(s),t.isArray(this.sourceData)?(this.doPrepend(),i.call(this)):e.call(this)},doPrepend:function(){null!==this.options.prepend&&void 0!==this.options.prepend&&(t.isArray(this.prependData)||(t.isFunction(this.options.prepend)&&(this.options.prepend=this.options.prepend.call(this.options.scope)),this.options.prepend=t.fn.editableutils.tryParseJson(this.options.prepend,!0),"string"==typeof this.options.prepend&&(this.options.prepend={"":this.options.prepend}),this.prependData=this.makeArray(this.options.prepend)),t.isArray(this.prependData)&&t.isArray(this.sourceData)&&(this.sourceData=this.prependData.concat(this.sourceData)))},renderList:function(){},value2htmlFinal:function(t,i){},makeArray:function(i){var e,s,n,o,a=[];if(!i||"string"==typeof i)return null;if(t.isArray(i)){o=function(t,i){return s={value:t,text:i},e++>=2?!1:void 0};for(var r=0;r
1&&(n.children&&(n.children=this.makeArray(n.children)),a.push(n))):a.push({value:n,text:n})}else t.each(i,function(t,i){a.push({value:t,text:i})});return a},option:function(t,i){this.options[t]=i,"source"===t&&(this.sourceData=null),"prepend"===t&&(this.prependData=null)}}),i.defaults=t.extend({},t.fn.editabletypes.abstractinput.defaults,{source:null,prepend:!1,sourceError:"Error when loading list",sourceCache:!0,sourceOptions:null}),t.fn.editabletypes.list=i}(window.jQuery),function(t){"use strict";var i=function(t){this.init("text",t,i.defaults)};t.fn.editableutils.inherit(i,t.fn.editabletypes.abstractinput),t.extend(i.prototype,{render:function(){this.renderClear(),this.setClass(),this.setAttr("placeholder")},activate:function(){this.$input.is(":visible")&&(this.$input.focus(),t.fn.editableutils.setCursorPosition(this.$input.get(0),this.$input.val().length),this.toggleClear&&this.toggleClear())},renderClear:function(){this.options.clear&&(this.$clear=t(''),this.$input.after(this.$clear).css("padding-right",24).keyup(t.proxy(function(i){if(!~t.inArray(i.keyCode,[40,38,9,13,27])){clearTimeout(this.t);var e=this;this.t=setTimeout(function(){e.toggleClear(i)},100)}},this)).parent().css("position","relative"),this.$clear.click(t.proxy(this.clear,this)))},postrender:function(){},toggleClear:function(t){if(this.$clear){var i=this.$input.val().length,e=this.$clear.is(":visible");i&&!e&&this.$clear.show(),!i&&e&&this.$clear.hide()}},clear:function(){this.$clear.hide(),this.$input.val("").focus()}}),i.defaults=t.extend({},t.fn.editabletypes.abstractinput.defaults,{tpl:'',placeholder:null,clear:!0}),t.fn.editabletypes.text=i}(window.jQuery),function(t){"use strict";var i=function(t){this.init("textarea",t,i.defaults)};t.fn.editableutils.inherit(i,t.fn.editabletypes.abstractinput),t.extend(i.prototype,{render:function(){this.setClass(),this.setAttr("placeholder"),this.setAttr("rows"),this.$input.keydown(function(i){i.ctrlKey&&13===i.which&&t(this).closest("form").submit()})},activate:function(){t.fn.editabletypes.text.prototype.activate.call(this)}}),i.defaults=t.extend({},t.fn.editabletypes.abstractinput.defaults,{tpl:"",inputclass:"input-large",placeholder:null,rows:7}),t.fn.editabletypes.textarea=i}(window.jQuery),function(t){"use strict";var i=function(t){this.init("select",t,i.defaults)};t.fn.editableutils.inherit(i,t.fn.editabletypes.list),t.extend(i.prototype,{renderList:function(){this.$input.empty();var i=function(e,s){var n;if(t.isArray(s))for(var o=0;o",n),s[o].children))):(n.value=s[o].value,s[o].disabled&&(n.disabled=!0),e.append(t("