phrasing 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +142 -0
- data/MIT-LICENSE +20 -0
- data/README.md +93 -0
- data/Rakefile +35 -0
- data/app/assets/fonts/icomoon.dev.svg +38 -0
- data/app/assets/fonts/icomoon.eot +0 -0
- data/app/assets/fonts/icomoon.svg +38 -0
- data/app/assets/fonts/icomoon.ttf +0 -0
- data/app/assets/fonts/icomoon.woff +0 -0
- data/app/assets/images/phrasing_information_icon.png +0 -0
- data/app/assets/javascripts/editor.js +318 -0
- data/app/assets/javascripts/head.js +423 -0
- data/app/assets/javascripts/phrasing.js.erb +148 -0
- data/app/assets/javascripts/spin.js +355 -0
- data/app/assets/stylesheets/phrasing.css.scss +240 -0
- data/app/assets/stylesheets/phrasing_edit_mode_bubble.css.scss +117 -0
- data/app/assets/stylesheets/phrasing_engine.css +470 -0
- data/app/assets/stylesheets/phrasing_fonts.css.scss +60 -0
- data/app/controllers/phrasing_phrases_controller.rb +126 -0
- data/app/helpers/inline_helper.rb +47 -0
- data/app/models/phrasing_phrase.rb +97 -0
- data/app/views/layouts/phrasing.html.haml +12 -0
- data/app/views/phrasing/_initializer.html.haml +25 -0
- data/app/views/phrasing/_menu.html.haml +8 -0
- data/app/views/phrasing/_messages.html.haml +4 -0
- data/app/views/phrasing/_production_warning.html.haml +4 -0
- data/app/views/phrasing_phrases/edit.html.haml +7 -0
- data/app/views/phrasing_phrases/help.html.haml +17 -0
- data/app/views/phrasing_phrases/import_export.html.haml +18 -0
- data/app/views/phrasing_phrases/index.html.haml +17 -0
- data/config/routes.rb +12 -0
- data/db/migrate/20120313191745_create_phrasing_phrases.rb +11 -0
- data/lib/phrasing.rb +72 -0
- data/lib/phrasing/ambiguous_phrases_error.rb +3 -0
- data/lib/phrasing/implementation.rb +19 -0
- data/lib/phrasing/phrasable_error_handler.rb +9 -0
- data/lib/phrasing/simple.rb +3 -0
- data/lib/phrasing/version.rb +3 -0
- data/lib/tasks/phrasing_tasks.rake +57 -0
- data/phrasing.gemspec +21 -0
- metadata +135 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
@font-face {
|
2
|
+
font-family: 'icomoon';
|
3
|
+
src:asset-url('icomoon.eot');
|
4
|
+
src:asset-url('icomoon.eot?#iefix') format('embedded-opentype'),
|
5
|
+
asset-url('icomoon.woff') format('woff'),
|
6
|
+
asset-url('icomoon.ttf') format('truetype'),
|
7
|
+
asset-url('icomoon.svg#icomoon') format('svg');
|
8
|
+
font-weight: normal;
|
9
|
+
font-style: normal;
|
10
|
+
}
|
11
|
+
|
12
|
+
/* Use the following CSS code if you want to use data attributes for inserting your icons */
|
13
|
+
[data-icon]:before {
|
14
|
+
font-family: 'icomoon';
|
15
|
+
content: attr(data-icon);
|
16
|
+
speak: none;
|
17
|
+
font-weight: normal;
|
18
|
+
font-variant: normal;
|
19
|
+
text-transform: none;
|
20
|
+
line-height: 1;
|
21
|
+
-webkit-font-smoothing: antialiased;
|
22
|
+
}
|
23
|
+
|
24
|
+
/* Use the following CSS code if you want to have a class per icon */
|
25
|
+
/*
|
26
|
+
Instead of a list of all class selectors,
|
27
|
+
you can use the generic selector below, but it's slower:
|
28
|
+
[class*="icon-"] {
|
29
|
+
*/
|
30
|
+
.icon-expand, .icon-target, .icon-contrast, .icon-floppy, .icon-contract, .icon-link, .icon-download-alt {
|
31
|
+
font-family: 'icomoon';
|
32
|
+
speak: none;
|
33
|
+
font-style: normal;
|
34
|
+
font-weight: normal;
|
35
|
+
font-variant: normal;
|
36
|
+
text-transform: none;
|
37
|
+
line-height: 1;
|
38
|
+
-webkit-font-smoothing: antialiased;
|
39
|
+
}
|
40
|
+
.icon-expand:before {
|
41
|
+
content: "\e000";
|
42
|
+
}
|
43
|
+
.icon-target:before {
|
44
|
+
content: "\e001";
|
45
|
+
}
|
46
|
+
.icon-contrast:before {
|
47
|
+
content: "\e002";
|
48
|
+
}
|
49
|
+
.icon-floppy:before {
|
50
|
+
content: "\e003";
|
51
|
+
}
|
52
|
+
.icon-contract:before {
|
53
|
+
content: "\e004";
|
54
|
+
}
|
55
|
+
.icon-link:before {
|
56
|
+
content: "\e005";
|
57
|
+
}
|
58
|
+
.icon-download-alt:before {
|
59
|
+
content: "\e006";
|
60
|
+
}
|
@@ -0,0 +1,126 @@
|
|
1
|
+
class PhrasingPhrasesController < ActionController::Base
|
2
|
+
|
3
|
+
layout 'phrasing'
|
4
|
+
|
5
|
+
include PhrasingHelper
|
6
|
+
|
7
|
+
before_filter :authorize_editor
|
8
|
+
|
9
|
+
def index
|
10
|
+
params[:locale] ||= I18n.default_locale
|
11
|
+
query = PhrasingPhrase
|
12
|
+
query = query.where(locale: params[:locale]) unless params[:locale].blank?
|
13
|
+
|
14
|
+
if params[:search] and !params[:search].blank?
|
15
|
+
key_like = PhrasingPhrase.arel_table[:key].matches("%#{params[:search]}%")
|
16
|
+
value_like = PhrasingPhrase.arel_table[:value].matches("%#{params[:search]}%")
|
17
|
+
@phrasing_phrases = query.where(key_like.or(value_like)).order(:key)
|
18
|
+
else
|
19
|
+
@phrasing_phrases = query.order(:key)
|
20
|
+
end
|
21
|
+
|
22
|
+
@locale_names = PhrasingPhrase.uniq.pluck(:locale)
|
23
|
+
end
|
24
|
+
|
25
|
+
def edit
|
26
|
+
@phrasing_phrase = PhrasingPhrase.find(params[:id])
|
27
|
+
end
|
28
|
+
|
29
|
+
def update
|
30
|
+
@phrasing_phrase = PhrasingPhrase.find(params[:id])
|
31
|
+
@phrasing_phrase.value = params[:phrasing_phrase][:value]
|
32
|
+
@phrasing_phrase.save!
|
33
|
+
|
34
|
+
respond_to do |format|
|
35
|
+
format.html do
|
36
|
+
redirect_to phrasing_phrases_path, notice: "#{@phrasing_phrase.key} updated!"
|
37
|
+
end
|
38
|
+
|
39
|
+
format.js do
|
40
|
+
render :json => @phrasing_phrase
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def import_export
|
46
|
+
end
|
47
|
+
|
48
|
+
def download
|
49
|
+
app_name = Rails.application.class.to_s.split("::").first
|
50
|
+
app_env = Rails.env
|
51
|
+
filename = "phrasing_phrases_#{app_name}_#{app_env}_#{Time.now.strftime("%Y_%m_%d_%H_%M_%S")}.yml"
|
52
|
+
send_data PhrasingPhrase.export_yaml, filename: filename
|
53
|
+
end
|
54
|
+
|
55
|
+
def upload
|
56
|
+
PhrasingPhrase.import_yaml(params["file"].tempfile)
|
57
|
+
redirect_to phrasing_phrases_path, notice: "YAML file uploaded successfully!"
|
58
|
+
rescue Exception => e
|
59
|
+
logger.info "\n#{e.class}\n#{e.message}"
|
60
|
+
flash[:alert] = "There was an error processing your upload! ##{e.message}"
|
61
|
+
render action: 'import_export', status: 400
|
62
|
+
end
|
63
|
+
|
64
|
+
def destroy
|
65
|
+
@phrasing_phrase = PhrasingPhrase.find(params[:id])
|
66
|
+
@phrasing_phrase.destroy
|
67
|
+
redirect_to phrasing_phrases_path, notice: "#{@phrasing_phrase.key} deleted!"
|
68
|
+
end
|
69
|
+
|
70
|
+
def help
|
71
|
+
end
|
72
|
+
|
73
|
+
def sync
|
74
|
+
if Phrasing.staging_server_endpoint.nil?
|
75
|
+
redirect_to :back, alert: "You didn't set your source server"
|
76
|
+
else
|
77
|
+
yaml = read_remote_yaml(Phrasing.staging_server_endpoint)
|
78
|
+
|
79
|
+
if yaml
|
80
|
+
PhrasingPhrase.import_yaml(yaml)
|
81
|
+
redirect_to :back, notice: "Translations synced from source server"
|
82
|
+
else
|
83
|
+
redirect_to :back
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def remote_update_phrase
|
90
|
+
klass, attribute = params[:klass], params[:attribute]
|
91
|
+
|
92
|
+
if Phrasing.is_whitelisted?(klass, attribute)
|
93
|
+
class_object = klass.classify.constantize
|
94
|
+
@object = class_object.where(id: params[:id]).first
|
95
|
+
@object.send("#{attribute}=",params[:new_value])
|
96
|
+
@object.save!
|
97
|
+
render :json => @object
|
98
|
+
else
|
99
|
+
render status: 403, text: "#{klass}.#{attribute} not whitelisted."
|
100
|
+
end
|
101
|
+
|
102
|
+
rescue ActiveRecord::RecordInvalid => e
|
103
|
+
render status: 403, text: e
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
|
108
|
+
def read_remote_yaml(url)
|
109
|
+
output = nil
|
110
|
+
begin
|
111
|
+
open(url, http_basic_authentication: [Phrasing.username, Phrasing.password]) do |remote|
|
112
|
+
output = remote.read()
|
113
|
+
end
|
114
|
+
rescue Exception => e
|
115
|
+
logger.fatal e
|
116
|
+
flash[:alert] = "Syncing failed: #{e}"
|
117
|
+
end
|
118
|
+
output
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
def authorize_editor
|
123
|
+
redirect_to root_path unless can_edit_phrases?
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module InlineHelper
|
2
|
+
|
3
|
+
def phrase(key, options = {}, *args)
|
4
|
+
key = key.to_s
|
5
|
+
if can_edit_phrases?
|
6
|
+
@record = PhrasingPhrase.where(key: key).first || PhrasingPhrase.create_phrase(key)
|
7
|
+
inline(@record, :value, options)
|
8
|
+
else
|
9
|
+
t(key, *args).html_safe
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def inline(record, field_name, options={})
|
14
|
+
return record.send(field_name).to_s.html_safe unless can_edit_phrases?
|
15
|
+
|
16
|
+
klass = 'phrasable'
|
17
|
+
klass += ' phrasable_on' if edit_mode_on?
|
18
|
+
klass += ' inverse' if options[:inverse]
|
19
|
+
|
20
|
+
url = phrasing_polymorphic_url(record, field_name)
|
21
|
+
|
22
|
+
content_tag(:span, { class: klass, contenteditable: edit_mode_on?, spellcheck: false, "data-url" => url}) do
|
23
|
+
(record.send(field_name) || record.try(:key) || "#{field_name}-#{record.id}").to_s.html_safe
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
alias_method :model_phrase, :inline
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def edit_mode_on?
|
33
|
+
if cookies["editing_mode"].nil?
|
34
|
+
cookies['editing_mode'] = "true"
|
35
|
+
true
|
36
|
+
else
|
37
|
+
cookies['editing_mode'] == "true"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def phrasing_polymorphic_url(record, attribute)
|
43
|
+
resource = Phrasing.route
|
44
|
+
"#{root_url}#{resource}/remote_update_phrase?klass=#{record.class.to_s}&id=#{record.id}&attribute=#{attribute}"
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
class PhrasingPhrase < ActiveRecord::Base
|
2
|
+
require 'phrasing/ambiguous_phrases_error'
|
3
|
+
|
4
|
+
unless ENV['PHRASING_DEBUG']
|
5
|
+
self.logger = Logger.new('/dev/null')
|
6
|
+
end
|
7
|
+
|
8
|
+
validates_presence_of :key, :locale
|
9
|
+
|
10
|
+
before_create :check_ambiguity
|
11
|
+
|
12
|
+
def self.create_phrase(key)
|
13
|
+
phrasing_phrase = PhrasingPhrase.new
|
14
|
+
phrasing_phrase.key = key
|
15
|
+
phrasing_phrase.value = key.to_s.humanize
|
16
|
+
phrasing_phrase.locale = I18n.locale
|
17
|
+
phrasing_phrase.save!
|
18
|
+
phrasing_phrase
|
19
|
+
end
|
20
|
+
|
21
|
+
module Serialize
|
22
|
+
|
23
|
+
def import_yaml(yaml)
|
24
|
+
hash = YAML.load(yaml)
|
25
|
+
hash.each do |locale, data|
|
26
|
+
hash_flatten(data).each do |key, value|
|
27
|
+
c = where(key: key, locale: locale).first
|
28
|
+
c ||= new(key: key, locale: locale)
|
29
|
+
c.value = value
|
30
|
+
c.save
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# {"foo"=>{"a"=>"1", "b"=>"2"}} ----> {"foo.a"=>1, "foo.b"=>2}
|
36
|
+
def hash_flatten(hash)
|
37
|
+
result = {}
|
38
|
+
hash.each do |key, value|
|
39
|
+
if value.is_a? Hash
|
40
|
+
hash_flatten(value).each { |k,v| result["#{key}.#{k}"] = v }
|
41
|
+
else
|
42
|
+
result[key] = value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
def export_yaml
|
49
|
+
hash = {}
|
50
|
+
where("value is not null").each do |c|
|
51
|
+
hash_fatten!(hash, [c.locale].concat(c.key.split(".")), c.value)
|
52
|
+
end
|
53
|
+
hash.to_yaml
|
54
|
+
end
|
55
|
+
|
56
|
+
# ({"a"=>{"b"=>{"e"=>"f"}}}, ["a","b","c"], "d") ----> {"a"=>{"b"=>{"c"=>"d", "e"=>"f"}}}
|
57
|
+
def hash_fatten!(hash, keys, value)
|
58
|
+
if keys.length == 1
|
59
|
+
hash[keys.first] = value
|
60
|
+
else
|
61
|
+
head = keys.first
|
62
|
+
rest = keys[1..-1]
|
63
|
+
hash[head] ||= {}
|
64
|
+
hash_fatten!(hash[head], rest, value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
extend Serialize
|
71
|
+
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def check_ambiguity
|
76
|
+
check_ambiguity_on_ancestors
|
77
|
+
check_ambiguity_on_successors
|
78
|
+
end
|
79
|
+
|
80
|
+
def check_ambiguity_on_ancestors
|
81
|
+
stripped_key = key
|
82
|
+
while stripped_key.include?('.')
|
83
|
+
stripped_key = stripped_key.split('.')[0..-2].join('.')
|
84
|
+
if PhrasingPhrase.where(key: stripped_key).count > 0
|
85
|
+
raise Phrasing::AmbiguousPhrasesError, "Ambiguous calling! There exists a '#{stripped_key}' key, unable to call a new key '#{key}'"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def check_ambiguity_on_successors
|
91
|
+
key_successor = "#{key}."
|
92
|
+
if PhrasingPhrase.where(PhrasingPhrase.arel_table[:key].matches("%#{key_successor}%")).count > 0
|
93
|
+
raise Phrasing::AmbiguousPhrasesError, "Ambiguous calling! There exists one or multiple keys beginning with '#{key_successor}', unable to call a new key '#{key}'"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
!!!
|
2
|
+
%html
|
3
|
+
%head
|
4
|
+
%title Phrasing
|
5
|
+
= stylesheet_link_tag "phrasing_engine", :media => "all"
|
6
|
+
%meta{:content => "text/html; charset=utf-8", "http-equiv" => "Content-Type"}/
|
7
|
+
%body
|
8
|
+
#page
|
9
|
+
= render 'phrasing/production_warning' if Rails.env.production?
|
10
|
+
= render 'phrasing/menu'
|
11
|
+
= render 'phrasing/messages'
|
12
|
+
= yield
|
@@ -0,0 +1,25 @@
|
|
1
|
+
- if can_edit_phrases?
|
2
|
+
.text-options#zenpenbubble
|
3
|
+
.options
|
4
|
+
%span.no-overflow
|
5
|
+
%span.lengthen.ui-inputs
|
6
|
+
%button.url.useicons 
|
7
|
+
%input.url-input{placeholder: "Type or Paste URL here", type: "text"}/
|
8
|
+
%button.bold b
|
9
|
+
%button.italic i
|
10
|
+
|
11
|
+
#phrasing-edit-mode-bubble
|
12
|
+
%p#edit-mode-headline Edit mode:
|
13
|
+
.onoffswitch
|
14
|
+
%input#edit-mode-onoffswitch.onoffswitch-checkbox{checked: "checked", name: "onoffswitch", type: "checkbox"}/
|
15
|
+
%label.onoffswitch-label{for: "edit-mode-onoffswitch"}
|
16
|
+
.onoffswitch-inner
|
17
|
+
.onoffswitch-switch
|
18
|
+
#phrasing-spinner
|
19
|
+
%p No changes made.
|
20
|
+
#view-all-phrases
|
21
|
+
%p= link_to "All phrases", phrasing_phrases_path
|
22
|
+
#phrasing-info-icon-container
|
23
|
+
= image_tag "phrasing_information_icon.png", class: "phrasing-info-icon"
|
24
|
+
#phrasing-info-container
|
25
|
+
%p Your content will be saved automatically 3 seconds after you stop typing.
|
@@ -0,0 +1,8 @@
|
|
1
|
+
#header
|
2
|
+
%h1= link_to "Phrasing", phrasing_phrases_path
|
3
|
+
%ul
|
4
|
+
- if root_path
|
5
|
+
%li= link_to "Home page", root_path
|
6
|
+
%li= link_to "Phrases", phrasing_phrases_path
|
7
|
+
%li= link_to "Import / Export", import_export_phrasing_phrases_path
|
8
|
+
%li= link_to "Help", help_phrasing_phrases_path
|
@@ -0,0 +1,7 @@
|
|
1
|
+
.edit
|
2
|
+
%h2= @phrasing_phrase.key
|
3
|
+
= button_to "Delete this item", phrasing_phrase_path(@phrasing_phrase), :method => :delete, :onclick => "return confirm('Are you sure?');"
|
4
|
+
= form_for @phrasing_phrase, :url => { :action => "update" } do |f|
|
5
|
+
= f.text_area :value, :rows => 12
|
6
|
+
= f.submit "Update"
|
7
|
+
= link_to "Cancel", phrasing_phrases_path
|
@@ -0,0 +1,17 @@
|
|
1
|
+
%div
|
2
|
+
%div
|
3
|
+
%p Phrasing is a Ruby on Rails engine that allows you to edit live website copy.
|
4
|
+
%div
|
5
|
+
%h2 How to use
|
6
|
+
%p Search for the copy text you want to edit. If Phrasing is currently controlling that content, it will appear when you search. If not, ask a developer to add it to Phrasing. You can filter by locale to control what copy is displayed in different languages.
|
7
|
+
%p Once copy appears, click the left hand side links to edit. Values with keys ending in "_html" will be parsed as literal html, so you can insert tags. All other values are parsed as plain text.
|
8
|
+
%div
|
9
|
+
%h2 Exporting and Importing
|
10
|
+
%p Phrasing text is data--not code. By default, it is stored in your server's database, and not the codebase that powers your website. Therefore, you may have to download/upload Phrasing data to transfer your copy edits between servers, or to safeguard them between database wipes. Phrasing exports (and is able to re-import) all of its data in a plaintext format called "YAML."
|
11
|
+
%h4 Exporting
|
12
|
+
%p You can get a YAML file of all your copy by clicking "Download" on the Import/Export page.
|
13
|
+
%h4 Importing
|
14
|
+
%p You can upload copy to Phrasing by uploading a properly formatted YAML file on the Import/Export page. Uploading copy won't delete any entries from Phrasing, but it may update them or create new ones. Use this feature to mass-update your copy from between servers -- e.g., make changes to copy on a test server, and then when you're satisfied, download the YAML file and upload it to your production site. Downloading your copy as YAML and then immediately re-uploading it will leave your copy unchanged.
|
15
|
+
%h4 Commiting copy to code
|
16
|
+
%p
|
17
|
+
If your copy has reached a stable state and you don't need to update it live anymore, you can download your copy as YAML and include it as part of the Ruby on Rails application underneath your main website. You can then remove Phrasing from the application entirely, and the website will still run exactly as before--it interfaces with Ruby on Rails's built-in translation mechanisms in a very unobtrusive way.
|
@@ -0,0 +1,18 @@
|
|
1
|
+
%h2 Import
|
2
|
+
%div
|
3
|
+
%p
|
4
|
+
Upload a YAML (.yml) file following
|
5
|
+
= succeed "." do
|
6
|
+
%a{:href => "http://guides.rubyonrails.org/i18n.html"} I18n conventions
|
7
|
+
= form_tag 'upload', :multipart => true, :'accept-charset' => 'UTF-8' do
|
8
|
+
= file_field_tag "file"
|
9
|
+
= submit_tag "Upload"
|
10
|
+
|
11
|
+
- unless Phrasing.staging_server_endpoint.nil? || Rails.env.staging?
|
12
|
+
%h2 Staging syncing
|
13
|
+
%div
|
14
|
+
= link_to "Sync from staging server", sync_phrasing_phrases_path
|
15
|
+
|
16
|
+
%h2 Export
|
17
|
+
%div
|
18
|
+
= link_to "Download as YAML", download_phrasing_phrases_path
|
@@ -0,0 +1,17 @@
|
|
1
|
+
#search
|
2
|
+
= form_tag phrasing_phrases_path, :method => :get do
|
3
|
+
- locale_options = options_for_select([nil] + @locale_names.map{|l| [l,l]}, params[:locale])
|
4
|
+
= select_tag 'locale', locale_options
|
5
|
+
= text_field_tag :search, params[:search], :placeholder => 'Search for keys or values', :size => 40
|
6
|
+
= submit_tag 'Search'
|
7
|
+
- if @phrasing_phrases.any?
|
8
|
+
%table
|
9
|
+
%tr
|
10
|
+
%th Key
|
11
|
+
%th Value
|
12
|
+
- @phrasing_phrases.each do |t|
|
13
|
+
%tr
|
14
|
+
%td.key
|
15
|
+
= link_to "#{t.key}", edit_phrasing_phrase_path(t)
|
16
|
+
%td.value
|
17
|
+
= t.value.try(:html_safe)
|