iye 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +44 -0
- data/Rakefile +9 -0
- data/bin/iye +6 -0
- data/iye.gemspec +40 -0
- data/lib/i18n_yaml_editor.rb +13 -0
- data/lib/i18n_yaml_editor/app.rb +46 -0
- data/lib/i18n_yaml_editor/category.rb +22 -0
- data/lib/i18n_yaml_editor/key.rb +30 -0
- data/lib/i18n_yaml_editor/store.rb +115 -0
- data/lib/i18n_yaml_editor/transformation.rb +35 -0
- data/lib/i18n_yaml_editor/translation.rb +19 -0
- data/lib/i18n_yaml_editor/web.rb +57 -0
- data/test/test_helper.rb +6 -0
- data/test/unit/test_app.rb +7 -0
- data/test/unit/test_category.rb +7 -0
- data/test/unit/test_key.rb +43 -0
- data/test/unit/test_store.rb +157 -0
- data/test/unit/test_transformation.rb +44 -0
- data/test/unit/test_translation.rb +16 -0
- data/views/categories.html.erb +9 -0
- data/views/layout.erb +139 -0
- data/views/translations.html.erb +44 -0
- metadata +122 -0
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# IYE
|
2
|
+
|
3
|
+
IYE - short for I18N YAML Editor - makes it easy to translate your Rails I18N files and
|
4
|
+
keep them up to date. Unlike a lot of other tools in this space, IYE works directly on the
|
5
|
+
YAML files instead of keeping a separate database in sync. This has several benefits:
|
6
|
+
|
7
|
+
* Branching and diffing is trivial
|
8
|
+
* It does not alter the workflow for developers etc., whom can continue editing the
|
9
|
+
YAML files directly
|
10
|
+
* If your YAML files are organized in subfolders, this structure is kept intact
|
11
|
+
|
12
|
+
![IYE yaml editor](http://f.cl.ly/items/2K2V2i3N2R2X1L2F051F/Sk%C3%A6rmbillede%202012-09-18%20kl.%2013.36.07.png)
|
13
|
+
|
14
|
+
## Prerequisites
|
15
|
+
|
16
|
+
You need to understand a few things about IYE for it to make sense, mainly:
|
17
|
+
|
18
|
+
* IYE does not create new keys - keys must exist for at least one locale in the YAML files
|
19
|
+
* IYE does not create new locales - at least one key must exist for each locale in the YAML files
|
20
|
+
|
21
|
+
## Workflow
|
22
|
+
|
23
|
+
1. Install IYE:
|
24
|
+
|
25
|
+
$ gem install iye
|
26
|
+
|
27
|
+
2. Navigate to the folder containing your YAML files and start IYE:
|
28
|
+
|
29
|
+
$ iye .
|
30
|
+
|
31
|
+
At this point IYE loads all translation keys for all locales, and creates any
|
32
|
+
keys that might be missing for existing locales.
|
33
|
+
|
34
|
+
3. Point browser at [http://localhost:5050](http://localhost:5050)
|
35
|
+
4. Make changes and press 'Save' - each time you do this, all the keys will be
|
36
|
+
written to their original YAML files, which you can confirm e.g. by using
|
37
|
+
`git diff`.
|
38
|
+
|
39
|
+
## Development
|
40
|
+
|
41
|
+
The source ships with a `config.ru` suitable for development use with `shotgun(1)`:
|
42
|
+
|
43
|
+
shotgun -p 5050
|
44
|
+
|
data/Rakefile
ADDED
data/bin/iye
ADDED
data/iye.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "iye"
|
3
|
+
s.version = "1.0.0"
|
4
|
+
s.date = "2012-09-18"
|
5
|
+
s.summary = "I18n Yaml Editor"
|
6
|
+
s.email = "hv@firmafon.dk"
|
7
|
+
s.homepage = "http://github.com/firmafon/iye"
|
8
|
+
s.description = "I18n YAML Editor"
|
9
|
+
s.authors = ["Harry Vangberg"]
|
10
|
+
s.executables << "iye"
|
11
|
+
s.files = [
|
12
|
+
"README.md",
|
13
|
+
"Rakefile",
|
14
|
+
"iye.gemspec",
|
15
|
+
"bin/iye",
|
16
|
+
"lib/i18n_yaml_editor.rb",
|
17
|
+
"lib/i18n_yaml_editor/app.rb",
|
18
|
+
"lib/i18n_yaml_editor/category.rb",
|
19
|
+
"lib/i18n_yaml_editor/key.rb",
|
20
|
+
"lib/i18n_yaml_editor/store.rb",
|
21
|
+
"lib/i18n_yaml_editor/transformation.rb",
|
22
|
+
"lib/i18n_yaml_editor/translation.rb",
|
23
|
+
"lib/i18n_yaml_editor/web.rb",
|
24
|
+
"views/layout.erb",
|
25
|
+
"views/categories.html.erb",
|
26
|
+
"views/translations.html.erb"
|
27
|
+
]
|
28
|
+
s.test_files = [
|
29
|
+
"test/test_helper.rb",
|
30
|
+
"test/unit/test_app.rb",
|
31
|
+
"test/unit/test_category.rb",
|
32
|
+
"test/unit/test_key.rb",
|
33
|
+
"test/unit/test_store.rb",
|
34
|
+
"test/unit/test_transformation.rb",
|
35
|
+
"test/unit/test_translation.rb"
|
36
|
+
]
|
37
|
+
s.add_dependency "cuba", ">= 3"
|
38
|
+
s.add_dependency "tilt", ">= 1.3"
|
39
|
+
s.add_dependency "psych", ">= 1.3.4"
|
40
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module I18nYamlEditor
|
2
|
+
class << self
|
3
|
+
attr_accessor :app
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
require "i18n_yaml_editor/app"
|
8
|
+
require "i18n_yaml_editor/category"
|
9
|
+
require "i18n_yaml_editor/key"
|
10
|
+
require "i18n_yaml_editor/store"
|
11
|
+
require "i18n_yaml_editor/transformation"
|
12
|
+
require "i18n_yaml_editor/translation"
|
13
|
+
require "i18n_yaml_editor/web"
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "psych"
|
4
|
+
require "yaml"
|
5
|
+
|
6
|
+
require "i18n_yaml_editor"
|
7
|
+
require "i18n_yaml_editor/web"
|
8
|
+
require "i18n_yaml_editor/store"
|
9
|
+
|
10
|
+
module I18nYamlEditor
|
11
|
+
class App
|
12
|
+
def initialize path
|
13
|
+
@path = File.expand_path(path)
|
14
|
+
@store = Store.new
|
15
|
+
I18nYamlEditor.app = self
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_accessor :store
|
19
|
+
|
20
|
+
def start
|
21
|
+
$stdout.puts " * Loading translations from #{@path}"
|
22
|
+
load_translations
|
23
|
+
|
24
|
+
$stdout.puts " * Creating missing translations"
|
25
|
+
store.create_missing_keys
|
26
|
+
|
27
|
+
$stdout.puts " * Starting web editor at port 5050"
|
28
|
+
Rack::Server.start :app => Web, :Port => 5050
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_translations
|
32
|
+
files = Dir[@path + "/**/*.yml"]
|
33
|
+
files.each {|file|
|
34
|
+
yaml = YAML.load_file(file)
|
35
|
+
store.from_yaml(yaml, file)
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def save_translations
|
40
|
+
files = store.to_yaml
|
41
|
+
files.each {|file, yaml|
|
42
|
+
File.open(file, "w", encoding: "utf-8") {|f| f << yaml.to_yaml}
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module I18nYamlEditor
|
6
|
+
class Category
|
7
|
+
attr_accessor :name, :keys
|
8
|
+
|
9
|
+
def initialize attributes={}
|
10
|
+
@name = attributes[:name]
|
11
|
+
@keys = Set.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_key key
|
15
|
+
self.keys.add(key)
|
16
|
+
end
|
17
|
+
|
18
|
+
def complete?
|
19
|
+
self.keys.all? {|key| key.complete?}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module I18nYamlEditor
|
6
|
+
class Key
|
7
|
+
attr_accessor :name, :translations
|
8
|
+
|
9
|
+
def initialize attributes={}
|
10
|
+
@name = attributes[:name]
|
11
|
+
@translations = Set.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_translation translation
|
15
|
+
self.translations.add(translation)
|
16
|
+
end
|
17
|
+
|
18
|
+
def category
|
19
|
+
@category ||= self.name.split(".").first
|
20
|
+
end
|
21
|
+
|
22
|
+
def complete?
|
23
|
+
self.translations.all? {|t| t.text.to_s !~ /\A\s*\z/} || empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def empty?
|
27
|
+
self.translations.all? {|t| t.text.to_s =~ /\A\s*\z/}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
require "i18n_yaml_editor/transformation"
|
7
|
+
require "i18n_yaml_editor/category"
|
8
|
+
require "i18n_yaml_editor/key"
|
9
|
+
require "i18n_yaml_editor/translation"
|
10
|
+
|
11
|
+
module I18nYamlEditor
|
12
|
+
class DuplicateTranslationError < StandardError; end
|
13
|
+
|
14
|
+
class Store
|
15
|
+
include Transformation
|
16
|
+
|
17
|
+
attr_accessor :categories, :keys, :translations, :locales
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@categories = {}
|
21
|
+
@keys = {}
|
22
|
+
@translations = {}
|
23
|
+
@locales = Set.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_translation translation
|
27
|
+
if existing = self.translations[translation.name]
|
28
|
+
message = "#{translation.name} detected in #{translation.file} and #{existing.file}"
|
29
|
+
raise DuplicateTranslationError.new(message)
|
30
|
+
end
|
31
|
+
|
32
|
+
self.translations[translation.name] = translation
|
33
|
+
|
34
|
+
add_locale(translation.locale)
|
35
|
+
|
36
|
+
key = (self.keys[translation.key] ||= Key.new(name: translation.key))
|
37
|
+
key.add_translation(translation)
|
38
|
+
|
39
|
+
category = (self.categories[key.category] ||= Category.new(name: key.category))
|
40
|
+
category.add_key(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_key key
|
44
|
+
self.keys[key.name] = key
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_locale locale
|
48
|
+
self.locales.add(locale)
|
49
|
+
end
|
50
|
+
|
51
|
+
def filter_keys options={}
|
52
|
+
filters = []
|
53
|
+
if options.has_key?(:key)
|
54
|
+
filters << lambda {|k| k.name =~ options[:key]}
|
55
|
+
end
|
56
|
+
if options.has_key?(:complete)
|
57
|
+
filters << lambda {|k| k.complete? == options[:complete]}
|
58
|
+
end
|
59
|
+
if options.has_key?(:empty)
|
60
|
+
filters << lambda {|k| k.empty? == options[:empty]}
|
61
|
+
end
|
62
|
+
if options.has_key?(:text)
|
63
|
+
filters << lambda {|k|
|
64
|
+
k.translations.any? {|t| t.text =~ options[:text]}
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
self.keys.select {|name, key|
|
69
|
+
filters.all? {|filter| filter.call(key)}
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_missing_keys
|
74
|
+
self.keys.each {|name, key|
|
75
|
+
missing_locales = self.locales - key.translations.map(&:locale)
|
76
|
+
missing_locales.each {|locale|
|
77
|
+
translation = key.translations.first
|
78
|
+
|
79
|
+
# this just replaces the locale part of the file name. should
|
80
|
+
# be possible to do in a simpler way. gsub, baby.
|
81
|
+
path = Pathname.new(translation.file)
|
82
|
+
dirs, file = path.split
|
83
|
+
file = file.to_s.split(".")
|
84
|
+
file[-2] = locale
|
85
|
+
file = file.join(".")
|
86
|
+
path = dirs.join(file).to_s
|
87
|
+
|
88
|
+
new_translation = Translation.new(name: "#{locale}.#{key.name}", file: path)
|
89
|
+
add_translation(new_translation)
|
90
|
+
}
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def from_yaml yaml, file=nil
|
95
|
+
translations = flatten_hash(yaml)
|
96
|
+
translations.each {|name, text|
|
97
|
+
translation = Translation.new(name: name, text: text, file: file)
|
98
|
+
add_translation(translation)
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_yaml
|
103
|
+
result = {}
|
104
|
+
files = self.translations.values.group_by(&:file)
|
105
|
+
files.each {|file, translations|
|
106
|
+
file_result = {}
|
107
|
+
translations.each {|translation|
|
108
|
+
file_result[translation.name] = translation.text
|
109
|
+
}
|
110
|
+
result[file] = nest_hash(file_result)
|
111
|
+
}
|
112
|
+
result
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module I18nYamlEditor
|
4
|
+
module Transformation
|
5
|
+
def flatten_hash hash, namespace=[], tree={}
|
6
|
+
hash.each {|key, value|
|
7
|
+
child_ns = namespace.dup << key
|
8
|
+
if value.is_a?(Hash)
|
9
|
+
flatten_hash value, child_ns, tree
|
10
|
+
else
|
11
|
+
tree[child_ns.join(".")] = value
|
12
|
+
end
|
13
|
+
}
|
14
|
+
tree
|
15
|
+
end
|
16
|
+
module_function :flatten_hash
|
17
|
+
|
18
|
+
def nest_hash hash
|
19
|
+
result = {}
|
20
|
+
hash.each {|key, value|
|
21
|
+
sub_result = result
|
22
|
+
keys = key.split(".")
|
23
|
+
keys.each_with_index {|k, idx|
|
24
|
+
if keys.size - 1 == idx
|
25
|
+
sub_result[k.to_sym] = value
|
26
|
+
else
|
27
|
+
sub_result = (sub_result[k.to_sym] ||= {})
|
28
|
+
end
|
29
|
+
}
|
30
|
+
}
|
31
|
+
result
|
32
|
+
end
|
33
|
+
module_function :nest_hash
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module I18nYamlEditor
|
4
|
+
class Translation
|
5
|
+
attr_accessor :name, :file, :text
|
6
|
+
|
7
|
+
def initialize attributes={}
|
8
|
+
@name, @file, @text = attributes.values_at(:name, :file, :text)
|
9
|
+
end
|
10
|
+
|
11
|
+
def key
|
12
|
+
@key ||= self.name.split(".")[1..-1].join(".")
|
13
|
+
end
|
14
|
+
|
15
|
+
def locale
|
16
|
+
@locale ||= self.name.split(".").first
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "cuba"
|
4
|
+
require "cuba/render"
|
5
|
+
|
6
|
+
require "i18n_yaml_editor/app"
|
7
|
+
|
8
|
+
module I18nYamlEditor
|
9
|
+
class Web < Cuba
|
10
|
+
plugin Cuba::Render
|
11
|
+
|
12
|
+
settings[:render][:template_engine] = "erb"
|
13
|
+
settings[:render][:views] = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "views"))
|
14
|
+
|
15
|
+
use Rack::ShowExceptions
|
16
|
+
|
17
|
+
def app
|
18
|
+
I18nYamlEditor.app
|
19
|
+
end
|
20
|
+
|
21
|
+
define do
|
22
|
+
on get, root do
|
23
|
+
on param("filters") do |filters|
|
24
|
+
options = {}
|
25
|
+
options[:key] = /#{filters["key"]}/ if filters["key"].to_s.size > 0
|
26
|
+
options[:text] = /#{filters["text"]}/i if filters["text"].to_s.size > 0
|
27
|
+
options[:complete] = false if filters["incomplete"] == "on"
|
28
|
+
options[:empty] = true if filters["empty"] == "on"
|
29
|
+
|
30
|
+
keys = app.store.filter_keys(options)
|
31
|
+
|
32
|
+
res.write view("translations.html", keys: keys, filters: filters)
|
33
|
+
end
|
34
|
+
|
35
|
+
on default do
|
36
|
+
categories = app.store.categories.sort
|
37
|
+
res.write view("categories.html", categories: categories, filters: {})
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
on post, "update" do
|
42
|
+
if translations = req["translations"]
|
43
|
+
translations.each {|name, text|
|
44
|
+
app.store.translations[name].text = text
|
45
|
+
}
|
46
|
+
app.save_translations
|
47
|
+
end
|
48
|
+
|
49
|
+
res.redirect "/?#{Rack::Utils.build_nested_query(filters: req["filters"])}"
|
50
|
+
end
|
51
|
+
|
52
|
+
on get, "debug" do
|
53
|
+
res.write partial("debug.html", translations: app.store.translations.values)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
require "i18n_yaml_editor/key"
|
5
|
+
|
6
|
+
class TestKey < MiniTest::Unit::TestCase
|
7
|
+
def test_category
|
8
|
+
key = Key.new(name: "session.login")
|
9
|
+
assert_equal "session", key.category
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_complete_with_text_for_all_translations
|
13
|
+
key = Key.new(name: "session.login")
|
14
|
+
key.add_translation Translation.new(name: "da.session.login", text: "Log ind")
|
15
|
+
key.add_translation Translation.new(name: "en.session.login", text: "Sign in")
|
16
|
+
|
17
|
+
assert key.complete?
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_complete_with_no_texts
|
21
|
+
key = Key.new(name: "session.login")
|
22
|
+
key.add_translation Translation.new(name: "da.session.login")
|
23
|
+
key.add_translation Translation.new(name: "en.session.login")
|
24
|
+
|
25
|
+
assert key.complete?
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_not_complete_without_text_for_some_translations
|
29
|
+
key = Key.new(name: "session.login")
|
30
|
+
key.add_translation Translation.new(name: "da.session.login", text: "Log ind")
|
31
|
+
key.add_translation Translation.new(name: "en.session.login")
|
32
|
+
|
33
|
+
assert_equal false, key.complete?
|
34
|
+
end
|
35
|
+
|
36
|
+
def testemptycomplete_with_no_texts
|
37
|
+
key = Key.new(name: "session.login")
|
38
|
+
key.add_translation Translation.new(name: "da.session.login")
|
39
|
+
key.add_translation Translation.new(name: "en.session.login")
|
40
|
+
|
41
|
+
assert key.empty?
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
require "i18n_yaml_editor/store"
|
5
|
+
|
6
|
+
class TestStore < MiniTest::Unit::TestCase
|
7
|
+
def test_add_translations
|
8
|
+
store = Store.new
|
9
|
+
translation = Translation.new(:name => "da.session.login")
|
10
|
+
|
11
|
+
store.add_translation(translation)
|
12
|
+
|
13
|
+
assert_equal 1, store.translations.size
|
14
|
+
assert_equal translation, store.translations[translation.name]
|
15
|
+
|
16
|
+
assert_equal 1, store.keys.size
|
17
|
+
assert_equal Set.new([translation]), store.keys["session.login"].translations
|
18
|
+
|
19
|
+
assert_equal 1, store.categories.size
|
20
|
+
assert_equal %w(session.login), store.categories["session"].keys.map(&:name)
|
21
|
+
|
22
|
+
assert_equal 1, store.locales.size
|
23
|
+
assert_equal %w(da), store.locales.to_a
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_add_duplicate_translation
|
27
|
+
store = Store.new
|
28
|
+
t1 = Translation.new(:name => "da.session.login")
|
29
|
+
t2 = Translation.new(:name => "da.session.login")
|
30
|
+
store.add_translation(t1)
|
31
|
+
|
32
|
+
assert_raises(DuplicateTranslationError) {
|
33
|
+
store.add_translation(t2)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_filter_keys_on_key
|
38
|
+
store = Store.new
|
39
|
+
store.add_key(Key.new(name: "session.login"))
|
40
|
+
store.add_key(Key.new(name: "session.logout"))
|
41
|
+
|
42
|
+
result = store.filter_keys(key: /login/)
|
43
|
+
|
44
|
+
assert_equal 1, result.size
|
45
|
+
assert_equal %w(session.login), result.keys
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_filter_keys_on_complete
|
49
|
+
store = Store.new
|
50
|
+
store.add_translation Translation.new(name: "da.session.login", text: "Log ind")
|
51
|
+
store.add_translation Translation.new(name: "en.session.login")
|
52
|
+
store.add_translation Translation.new(name: "da.session.logout", text: "Log ud")
|
53
|
+
|
54
|
+
result = store.filter_keys(complete: false)
|
55
|
+
|
56
|
+
assert_equal 1, result.size
|
57
|
+
assert_equal %w(session.login), result.keys
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_filter_keys_on_empty
|
61
|
+
store = Store.new
|
62
|
+
store.add_translation Translation.new(name: "da.session.login", text: "Log ind")
|
63
|
+
store.add_translation Translation.new(name: "da.session.logout")
|
64
|
+
|
65
|
+
result = store.filter_keys(empty: true)
|
66
|
+
|
67
|
+
assert_equal 1, result.size
|
68
|
+
assert_equal %w(session.logout), result.keys
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_filter_keys_on_text
|
72
|
+
store = Store.new
|
73
|
+
store.add_translation Translation.new(name: "da.session.login", text: "Log ind")
|
74
|
+
store.add_translation Translation.new(name: "da.session.logout", text: "Log ud")
|
75
|
+
store.add_translation Translation.new(name: "da.app.name", text: "Translator")
|
76
|
+
|
77
|
+
result = store.filter_keys(text: /Log/)
|
78
|
+
|
79
|
+
assert_equal 2, result.size
|
80
|
+
assert_equal %w(session.login session.logout).sort, result.keys.sort
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_create_missing_translations
|
84
|
+
store = Store.new
|
85
|
+
store.add_translation Translation.new(name: "da.session.login", text: "Log ind", file: "/tmp/session.da.yml")
|
86
|
+
store.add_locale("en")
|
87
|
+
|
88
|
+
store.create_missing_keys
|
89
|
+
|
90
|
+
assert(translation = store.translations["en.session.login"])
|
91
|
+
assert_equal "en.session.login", translation.name
|
92
|
+
assert_equal "/tmp/session.en.yml", translation.file
|
93
|
+
assert_nil translation.text
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_create_missing_translations_in_top_level_file
|
97
|
+
store = Store.new
|
98
|
+
store.add_translation Translation.new(name: "da.app_name", text: "Oversætter", file: "/tmp/da.yml")
|
99
|
+
store.add_locale("en")
|
100
|
+
|
101
|
+
store.create_missing_keys
|
102
|
+
|
103
|
+
assert(translation = store.translations["en.app_name"])
|
104
|
+
assert_equal "en.app_name", translation.name
|
105
|
+
assert_equal "/tmp/en.yml", translation.file
|
106
|
+
assert_nil translation.text
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_from_yaml
|
110
|
+
input = {
|
111
|
+
da: {
|
112
|
+
session: {login: "Log ind"}
|
113
|
+
}
|
114
|
+
}
|
115
|
+
store = Store.new
|
116
|
+
|
117
|
+
store.from_yaml(input)
|
118
|
+
|
119
|
+
assert_equal 1, store.translations.size
|
120
|
+
translation = store.translations["da.session.login"]
|
121
|
+
assert_equal "da.session.login", translation.name
|
122
|
+
assert_equal "Log ind", translation.text
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_to_yaml
|
126
|
+
expected = {
|
127
|
+
"/tmp/session.da.yml" => {
|
128
|
+
da: {
|
129
|
+
session: {
|
130
|
+
login: "Log ind",
|
131
|
+
logout: "Log ud"
|
132
|
+
}
|
133
|
+
}
|
134
|
+
},
|
135
|
+
"/tmp/session.en.yml" => {
|
136
|
+
en: {
|
137
|
+
session: {
|
138
|
+
login: "Sign in"
|
139
|
+
}
|
140
|
+
}
|
141
|
+
},
|
142
|
+
"/tmp/da.yml" => {
|
143
|
+
da: {
|
144
|
+
app_name: "Oversætter"
|
145
|
+
}
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
store = Store.new
|
150
|
+
store.add_translation Translation.new(name: "da.session.login", text: "Log ind", file: "/tmp/session.da.yml")
|
151
|
+
store.add_translation Translation.new(name: "en.session.login", text: "Sign in", file: "/tmp/session.en.yml")
|
152
|
+
store.add_translation Translation.new(name: "da.session.logout", text: "Log ud", file: "/tmp/session.da.yml")
|
153
|
+
store.add_translation Translation.new(name: "da.app_name", text: "Oversætter", file: "/tmp/da.yml")
|
154
|
+
|
155
|
+
assert_equal expected, store.to_yaml
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
require "i18n_yaml_editor/transformation"
|
5
|
+
|
6
|
+
class TestTransformation < MiniTest::Unit::TestCase
|
7
|
+
def test_flatten_hash
|
8
|
+
input = {
|
9
|
+
da: {
|
10
|
+
session: {login: "Log ind", logout: "Log ud"}
|
11
|
+
},
|
12
|
+
en: {
|
13
|
+
session: {login: "Log in", logout: "Log out"}
|
14
|
+
}
|
15
|
+
}
|
16
|
+
expected = {
|
17
|
+
"da.session.login" => "Log ind",
|
18
|
+
"da.session.logout" => "Log ud",
|
19
|
+
"en.session.login" => "Log in",
|
20
|
+
"en.session.logout" => "Log out"
|
21
|
+
}
|
22
|
+
|
23
|
+
assert_equal expected, Transformation.flatten_hash(input)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_nest_hash
|
27
|
+
input = {
|
28
|
+
"da.session.login" => "Log ind",
|
29
|
+
"da.session.logout" => "Log ud",
|
30
|
+
"en.session.login" => "Log in",
|
31
|
+
"en.session.logout" => "Log out"
|
32
|
+
}
|
33
|
+
expected = {
|
34
|
+
da: {
|
35
|
+
session: {login: "Log ind", logout: "Log ud"}
|
36
|
+
},
|
37
|
+
en: {
|
38
|
+
session: {login: "Log in", logout: "Log out"}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
assert_equal expected, Transformation.nest_hash(input)
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
require "i18n_yaml_editor/translation"
|
5
|
+
|
6
|
+
class TestTranslation < MiniTest::Unit::TestCase
|
7
|
+
def test_key
|
8
|
+
translation = Translation.new(name: "da.session.login")
|
9
|
+
assert_equal "session.login", translation.key
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_locale
|
13
|
+
translation = Translation.new(name: "da.session.login")
|
14
|
+
assert_equal "da", translation.locale
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<table class="categories" cellspacing="0">
|
2
|
+
<% categories.each do |name, category| %>
|
3
|
+
<tr class="category <%= "incomplete" unless category.complete? %>">
|
4
|
+
<td class="key">
|
5
|
+
<a class="text" href="/?filters[key]=^<%= category.name %>"><%= category.name %></a>
|
6
|
+
</td>
|
7
|
+
</tr>
|
8
|
+
<% end %>
|
9
|
+
</table>
|
data/views/layout.erb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<title>IYE</title>
|
4
|
+
<style>
|
5
|
+
/* GENERAL */
|
6
|
+
body {
|
7
|
+
font-family: sans-serif;
|
8
|
+
font-size: 0.7em;
|
9
|
+
margin: 0;
|
10
|
+
margin-top: 60px;
|
11
|
+
}
|
12
|
+
|
13
|
+
/* HEADER */
|
14
|
+
.header {
|
15
|
+
margin: 0;
|
16
|
+
padding: 1em;
|
17
|
+
background-color: #fff;
|
18
|
+
z-index: 999 !important;
|
19
|
+
position: fixed;
|
20
|
+
top: 0;
|
21
|
+
width: 100%;
|
22
|
+
-webkit-box-shadow: 0 8px 6px -6px #ccc;
|
23
|
+
-moz-box-shadow: 0 8px 6px -6px #ccc;
|
24
|
+
box-shadow: 0 8px 6px -6px #ccc;
|
25
|
+
}
|
26
|
+
|
27
|
+
.header h1, .header form {
|
28
|
+
display: inline;
|
29
|
+
vertical-align: middle;
|
30
|
+
}
|
31
|
+
|
32
|
+
.header h1 {
|
33
|
+
margin-right: 2em;
|
34
|
+
}
|
35
|
+
|
36
|
+
.pull-right {
|
37
|
+
float:right;
|
38
|
+
padding-right:20px;
|
39
|
+
}
|
40
|
+
|
41
|
+
/* TRANSLATIONS */
|
42
|
+
table.keys {
|
43
|
+
width: 100%;
|
44
|
+
}
|
45
|
+
|
46
|
+
table.keys .key {
|
47
|
+
padding-left: 0.5em;
|
48
|
+
}
|
49
|
+
|
50
|
+
table.keys tr.translation > td {
|
51
|
+
border-bottom: 1px solid #bbb;
|
52
|
+
}
|
53
|
+
|
54
|
+
table.keys tr.translation.incomplete .key .text {
|
55
|
+
border-bottom: 3px solid #f33;
|
56
|
+
}
|
57
|
+
|
58
|
+
table.keys tr.translation.empty .key .text {
|
59
|
+
border-bottom: 3px solid #33f;
|
60
|
+
}
|
61
|
+
|
62
|
+
table.keys td.key {
|
63
|
+
width: 300px;
|
64
|
+
}
|
65
|
+
|
66
|
+
table.locales td.value input {
|
67
|
+
width: 600px;
|
68
|
+
}
|
69
|
+
|
70
|
+
/* CATEGORIES */
|
71
|
+
|
72
|
+
table.categories {
|
73
|
+
width: 100%;
|
74
|
+
}
|
75
|
+
|
76
|
+
table.categories .key {
|
77
|
+
padding-left: 0.5em;
|
78
|
+
}
|
79
|
+
|
80
|
+
table.categories tr.category a {
|
81
|
+
text-decoration: none;
|
82
|
+
}
|
83
|
+
|
84
|
+
table.categories tr.category > td {
|
85
|
+
border-bottom: 1px solid #bbb;
|
86
|
+
}
|
87
|
+
|
88
|
+
table.categories tr.category > td {
|
89
|
+
padding-top: 1em;
|
90
|
+
padding-bottom: 1em;
|
91
|
+
}
|
92
|
+
|
93
|
+
table.categories tr.category.incomplete .key .text {
|
94
|
+
border-bottom: 3px solid #f33;
|
95
|
+
}
|
96
|
+
</style>
|
97
|
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"></script>
|
98
|
+
</head>
|
99
|
+
|
100
|
+
<body>
|
101
|
+
<div class="header">
|
102
|
+
<h1>IYE</h1>
|
103
|
+
|
104
|
+
<form action="/" method="get">
|
105
|
+
<label>
|
106
|
+
Key:
|
107
|
+
<input name="filters[key]" type="text" value="<%= filters["key"] %>" />
|
108
|
+
</label>
|
109
|
+
|
110
|
+
<label>
|
111
|
+
Text:
|
112
|
+
<input name="filters[text]" type="text" value="<%= filters["text"] %>" />
|
113
|
+
</label>
|
114
|
+
|
115
|
+
<label>
|
116
|
+
<input name="filters[incomplete]" type="checkbox" <%= "checked=checked" if filters["incomplete"] == "on" %> />
|
117
|
+
Incomplete
|
118
|
+
</label>
|
119
|
+
<label>
|
120
|
+
<input name="filters[empty]" type="checkbox" <%= "checked=checked" if filters["empty"] == "on" %> />
|
121
|
+
Empty
|
122
|
+
</label>
|
123
|
+
|
124
|
+
<input type="submit" value="Filter" />
|
125
|
+
</form>
|
126
|
+
|
127
|
+
<form action="/" method="get">
|
128
|
+
<input type="submit" value="Clear" />
|
129
|
+
</form>
|
130
|
+
|
131
|
+
<div class="pull-right">
|
132
|
+
</div>
|
133
|
+
</div>
|
134
|
+
|
135
|
+
<div class="content">
|
136
|
+
<%= content %>
|
137
|
+
</div>
|
138
|
+
</body>
|
139
|
+
</html>
|
@@ -0,0 +1,44 @@
|
|
1
|
+
<form id="updateform" action="/update" method="post">
|
2
|
+
<% filters.each do |filter, value| %>
|
3
|
+
<input name="filters[<%= filter %>" type="hidden" value="<%= value %>"/>
|
4
|
+
<% end %>
|
5
|
+
|
6
|
+
<table class="keys" cellspacing="0">
|
7
|
+
<% keys.each do |name, key| %>
|
8
|
+
<tr class="translation
|
9
|
+
<%= "incomplete" unless key.complete? %>
|
10
|
+
<%= "empty" if key.empty? %>
|
11
|
+
">
|
12
|
+
<td class="key">
|
13
|
+
<span class="text"><%= key.name %></span>
|
14
|
+
</td>
|
15
|
+
<td class="translations">
|
16
|
+
<table class="locales">
|
17
|
+
<% key.translations.sort_by(&:locale).each do |translation| %>
|
18
|
+
<tr>
|
19
|
+
<td class="locale"><%= translation.locale %>:</td>
|
20
|
+
<td class="value">
|
21
|
+
<% if translation.text.nil? || translation.text.is_a?(String) %>
|
22
|
+
<input type="text" name="translations[<%= translation.name %>]" value="<%= translation.text %>" />
|
23
|
+
<% else %>
|
24
|
+
[read only]
|
25
|
+
<%= translation.text.class.name %>
|
26
|
+
<%= Rack::Utils.escape_html translation.text.inspect %>
|
27
|
+
<% end %>
|
28
|
+
</td>
|
29
|
+
</tr>
|
30
|
+
<% end %>
|
31
|
+
</table>
|
32
|
+
</td>
|
33
|
+
</tr>
|
34
|
+
<% end %>
|
35
|
+
</table>
|
36
|
+
|
37
|
+
<p class="pull-right"><input type="submit" id="save" value="Save" /></p>
|
38
|
+
</form>
|
39
|
+
<script type="text/javascript">
|
40
|
+
$('.header .pull-right').html("<button id='save-on-top'>Save</button>")
|
41
|
+
$('#save-on-top').click(function() {
|
42
|
+
$('#updateform').submit();
|
43
|
+
});
|
44
|
+
</script>
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: iye
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Harry Vangberg
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: cuba
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '3'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: tilt
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '1.3'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '1.3'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: psych
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.3.4
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.3.4
|
62
|
+
description: I18n YAML Editor
|
63
|
+
email: hv@firmafon.dk
|
64
|
+
executables:
|
65
|
+
- iye
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- iye.gemspec
|
72
|
+
- bin/iye
|
73
|
+
- lib/i18n_yaml_editor.rb
|
74
|
+
- lib/i18n_yaml_editor/app.rb
|
75
|
+
- lib/i18n_yaml_editor/category.rb
|
76
|
+
- lib/i18n_yaml_editor/key.rb
|
77
|
+
- lib/i18n_yaml_editor/store.rb
|
78
|
+
- lib/i18n_yaml_editor/transformation.rb
|
79
|
+
- lib/i18n_yaml_editor/translation.rb
|
80
|
+
- lib/i18n_yaml_editor/web.rb
|
81
|
+
- views/layout.erb
|
82
|
+
- views/categories.html.erb
|
83
|
+
- views/translations.html.erb
|
84
|
+
- test/test_helper.rb
|
85
|
+
- test/unit/test_app.rb
|
86
|
+
- test/unit/test_category.rb
|
87
|
+
- test/unit/test_key.rb
|
88
|
+
- test/unit/test_store.rb
|
89
|
+
- test/unit/test_transformation.rb
|
90
|
+
- test/unit/test_translation.rb
|
91
|
+
homepage: http://github.com/firmafon/iye
|
92
|
+
licenses: []
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
none: false
|
105
|
+
requirements:
|
106
|
+
- - ! '>='
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
requirements: []
|
110
|
+
rubyforge_project:
|
111
|
+
rubygems_version: 1.8.23
|
112
|
+
signing_key:
|
113
|
+
specification_version: 3
|
114
|
+
summary: I18n Yaml Editor
|
115
|
+
test_files:
|
116
|
+
- test/test_helper.rb
|
117
|
+
- test/unit/test_app.rb
|
118
|
+
- test/unit/test_category.rb
|
119
|
+
- test/unit/test_key.rb
|
120
|
+
- test/unit/test_store.rb
|
121
|
+
- test/unit/test_transformation.rb
|
122
|
+
- test/unit/test_translation.rb
|