iye 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+

|
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
|