phrasing 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +10 -0
  3. data/Gemfile.lock +142 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +93 -0
  6. data/Rakefile +35 -0
  7. data/app/assets/fonts/icomoon.dev.svg +38 -0
  8. data/app/assets/fonts/icomoon.eot +0 -0
  9. data/app/assets/fonts/icomoon.svg +38 -0
  10. data/app/assets/fonts/icomoon.ttf +0 -0
  11. data/app/assets/fonts/icomoon.woff +0 -0
  12. data/app/assets/images/phrasing_information_icon.png +0 -0
  13. data/app/assets/javascripts/editor.js +318 -0
  14. data/app/assets/javascripts/head.js +423 -0
  15. data/app/assets/javascripts/phrasing.js.erb +148 -0
  16. data/app/assets/javascripts/spin.js +355 -0
  17. data/app/assets/stylesheets/phrasing.css.scss +240 -0
  18. data/app/assets/stylesheets/phrasing_edit_mode_bubble.css.scss +117 -0
  19. data/app/assets/stylesheets/phrasing_engine.css +470 -0
  20. data/app/assets/stylesheets/phrasing_fonts.css.scss +60 -0
  21. data/app/controllers/phrasing_phrases_controller.rb +126 -0
  22. data/app/helpers/inline_helper.rb +47 -0
  23. data/app/models/phrasing_phrase.rb +97 -0
  24. data/app/views/layouts/phrasing.html.haml +12 -0
  25. data/app/views/phrasing/_initializer.html.haml +25 -0
  26. data/app/views/phrasing/_menu.html.haml +8 -0
  27. data/app/views/phrasing/_messages.html.haml +4 -0
  28. data/app/views/phrasing/_production_warning.html.haml +4 -0
  29. data/app/views/phrasing_phrases/edit.html.haml +7 -0
  30. data/app/views/phrasing_phrases/help.html.haml +17 -0
  31. data/app/views/phrasing_phrases/import_export.html.haml +18 -0
  32. data/app/views/phrasing_phrases/index.html.haml +17 -0
  33. data/config/routes.rb +12 -0
  34. data/db/migrate/20120313191745_create_phrasing_phrases.rb +11 -0
  35. data/lib/phrasing.rb +72 -0
  36. data/lib/phrasing/ambiguous_phrases_error.rb +3 -0
  37. data/lib/phrasing/implementation.rb +19 -0
  38. data/lib/phrasing/phrasable_error_handler.rb +9 -0
  39. data/lib/phrasing/simple.rb +3 -0
  40. data/lib/phrasing/version.rb +3 -0
  41. data/lib/tasks/phrasing_tasks.rake +57 -0
  42. data/phrasing.gemspec +21 -0
  43. 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 &#xe005;
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,4 @@
1
+ - if notice
2
+ .notice= notice
3
+ - if alert
4
+ .alert=alert
@@ -0,0 +1,4 @@
1
+ .alert
2
+ %h1 You're in production!
3
+ - unless Phrasing.staging_server_endpoint.nil?
4
+ %p You should do your changes in #{link_to("staging", Phrasing.staging_server_endpoint.gsub("/download", ""))} and then #{link_to("sync them",sync_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)