rosette 0.1.1 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '03866ba93b8b990e22574cb0d1f84a6e2059988ca1bd8e091ab8551aaa4db1d9'
4
- data.tar.gz: 26c70b8d2e6999dde21bd11fe0972505d2fc9b31b554ece1528f2f50aedd9dae
3
+ metadata.gz: 97ee90651ff8eb4ca983a10bf740934c878df05922013c149306b42e56778f21
4
+ data.tar.gz: 78b300250348617901cd7d2165b9fe0763ad49bf3327850ffdb2852d9dd55678
5
5
  SHA512:
6
- metadata.gz: d29a828701e75efa5654cacdfc353c34066dae80befff846f326ecf24611aa27396fdd8f0da8f233fd59fc818c9cb9867d1993b35e72d0c02aca392bb0acb171
7
- data.tar.gz: f8a993fe6be29486261e7495e2992610e14af47d9e7538ed54ac95ddf992f7122c661cf617a4fa650e008767b9327dc19a4ee6ee17b3ff896ef5a1e99204cc6a
6
+ metadata.gz: 182c58aa6fa14ba3b25aaf89d11dcc8e1c461fda6d1fd92b34ab709f98eade14da02d6afd66b994c4ddb2191f07ae5c5db281b131c2dfa280ff6c94bdb616761
7
+ data.tar.gz: d6a1b2bb51890b68a9f33d9bd3aa799a55a7cc6135abdd614747ca2118bf6a139c50ae0874f5d99dcd5fa160d700c5a344bc5eb333bb4fe229549c4724bf74ee
data/README.md CHANGED
@@ -1,10 +1,9 @@
1
1
  # Rosette
2
- Short description and motivation.
3
2
 
4
- ## Usage
5
- How to use my plugin.
3
+ Rosette is a Ruby on Rails engine that helps you add missing translations to your application. If your main app is configured to raise on missing translations, Rosette will catch any `I18n::MissingTranslationData` error and display a form to add the missing translations. The form includes an input for each available locale set by `config.i18n.available_locales`.
6
4
 
7
5
  ## Installation
6
+
8
7
  Add this line to your application's Gemfile:
9
8
 
10
9
  ```ruby
@@ -16,13 +15,22 @@ And then execute:
16
15
  $ bundle
17
16
  ```
18
17
 
19
- Or install it yourself as:
20
- ```bash
21
- $ gem install rosette
18
+ Make sure your app raises error for missing translations in development:
19
+
20
+ ```ruby
21
+ # config/environments/development.rb
22
+
23
+ config.i18n.raise_on_missing_translations = true
22
24
  ```
23
25
 
24
- ## Contributing
25
- Contribution directions go here.
26
+ Whether you currently use or are interested in starting to use [i18n-task to normalize](https://github.com/glebm/i18n-tasks#normalize-data) your locales files, add this initializer:
27
+
28
+ ```ruby
29
+ # config/initializers/rosette.rb
30
+
31
+ Rosette.normalize = true
32
+ ```
26
33
 
27
34
  ## License
35
+
28
36
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,15 +1,83 @@
1
- /*
2
- * This is a manifest file that'll be compiled into application.css, which will include all the files
3
- * listed below.
4
- *
5
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
- * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
- *
8
- * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
- * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
- * files in this directory. Styles in this file should be added after the last require_* statement.
11
- * It is generally better to create a new file per style scope.
12
- *
13
- *= require_tree .
14
- *= require_self
15
- */
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500');
2
+
3
+ .text-center {
4
+ text-align: center;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Inter', sans-serif;
9
+ background-color: white;
10
+ margin: 5rem;
11
+ }
12
+
13
+ .form-wrapper {
14
+ background-color: rgb(241, 237, 237);
15
+ padding: 2.5rem;
16
+ margin: 32px auto;
17
+ border-radius: 0.5rem;
18
+ max-width: 38rem;
19
+ box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
20
+ position: relative;
21
+ }
22
+
23
+ .form {
24
+ display: flex;
25
+ flex-direction: column;
26
+ }
27
+
28
+ .input-text {
29
+ margin-bottom: 1rem;
30
+ padding: 1rem;
31
+ border-radius: .375rem;
32
+ border-color: rgb(211, 211, 211);
33
+ border: solid 1px rgba(0, 0, 0, 0.16);
34
+ box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
35
+ background-color: rgb(251, 249, 249);
36
+ }
37
+
38
+ .input-text:focus {
39
+ outline: 1rem solid transparent;
40
+ outline-offset: 1rem;
41
+ border-color: rgb(163, 163, 163);
42
+ }
43
+
44
+ label {
45
+ margin-bottom: 1rem;
46
+ }
47
+
48
+ .button {
49
+ background-color: rgb(220, 38, 38);
50
+ color: white;
51
+ padding: 0.75rem 5rem;
52
+ border-color: transparent;
53
+ border-radius: .375rem;
54
+ margin: 1rem auto 0 auto;
55
+ font-size: 1rem;
56
+ line-height: 1.25rem;
57
+ cursor: pointer;
58
+ transition: background-color 0.8s;
59
+ }
60
+
61
+ .button:hover {
62
+ background-color: rgb(235, 63, 54);
63
+ }
64
+
65
+ .note {
66
+ font-size: .75rem;
67
+ color: rgb(107, 114, 128);
68
+ margin-top: 1rem;
69
+ }
70
+
71
+ .error {
72
+ color: #856404;
73
+ background-color: #fff3cd;
74
+ border-color: #ffeeba;
75
+ padding: 0.75rem 1.25rem;
76
+ margin-bottom: 1rem;
77
+ border-radius: 0.25rem;
78
+ }
79
+
80
+ .locale {
81
+ color: rgb(220, 38, 38);
82
+ font-weight: 500;
83
+ }
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rosette
4
+ class TranslationsController < ApplicationController
5
+ def create
6
+ available_locales.each { |locale, translation| Manager.create(locale, key, translation) }
7
+
8
+ Manager.normalize!
9
+
10
+ redirect_to redirect_path, notice: 'Translation(s) added'
11
+ rescue StandardError => e
12
+ @error_message = e.message
13
+ render_rosette_new(key, redirect_path)
14
+ end
15
+
16
+ private
17
+
18
+ def translations_params
19
+ params.require(:translations).permit(:key, :redirect_path, available_locales: {})
20
+ end
21
+
22
+ def available_locales
23
+ translations_params[:available_locales]
24
+ end
25
+
26
+ def key
27
+ translations_params[:key]
28
+ end
29
+
30
+ def redirect_path
31
+ URI.parse(translations_params[:redirect_path]).to_s
32
+ end
33
+ end
34
+ end
@@ -1,15 +1,15 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
- <head>
4
- <title>Rosette</title>
5
- <%= csrf_meta_tags %>
6
- <%= csp_meta_tag %>
3
+ <head>
4
+ <title>Rosette</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
7
 
8
- <%= stylesheet_link_tag "rosette/application", media: "all" %>
9
- </head>
10
- <body>
8
+ <%= stylesheet_link_tag "rosette/application", media: "all" %>
9
+ </head>
10
+ <body>
11
11
 
12
- <%= yield %>
12
+ <%= yield %>
13
13
 
14
- </body>
14
+ </body>
15
15
  </html>
@@ -0,0 +1,28 @@
1
+ <h1 class="text-center">Add missing translation</h1>
2
+
3
+ <p class="text-center locale"><%= key %></p>
4
+
5
+ <div class="form-wrapper">
6
+ <%= form_with scope: :translations, url: rosette.translations_path, class: 'form' do |f| %>
7
+ <% if @error_message %>
8
+ <p class="error"><%= @error_message %></p>
9
+ <% end %>
10
+
11
+ <%= f.hidden_field :key, value: key %>
12
+ <%= f.hidden_field :redirect_path, value: redirect_path %>
13
+
14
+ <%= f.fields_for :available_locales do |a| %>
15
+ <% Rosette.available_locales.each do |locale| %>
16
+ <%= a.label locale, "#{locale.capitalize} translation:" %>
17
+ <%= a.text_field locale, required: false, label: false, class: 'input-text' %>
18
+ <% end %>
19
+ <% end %>
20
+
21
+ <%= f.button 'Submit', class: 'button' %>
22
+ <% end %>
23
+
24
+ </div>
25
+
26
+ <div class="note text-center">
27
+ If you prefer to add the translation directly to the yaml file, you can choose not to fill in the input field.
28
+ </div>
data/bin/rails ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path("..", __dir__)
6
+ ENGINE_PATH = File.expand_path("../lib/rosette/engine", __dir__)
7
+ APP_PATH = File.expand_path("../spec/example_app/config/application", __dir__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
11
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
12
+
13
+ require "rails"
14
+
15
+ require "action_controller/railtie"
16
+ require "action_view/railtie"
17
+ require "active_model/railtie"
18
+ require "active_record/railtie"
19
+ require "rails/engine/commands"
data/bin/rosette ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require File.join(Dir.pwd, 'config/environment')
6
+ rescue LoadError
7
+ puts <<~ERR
8
+ Could not load your Rails app
9
+ =============================
10
+ Rosette assumes you have a config/environment.rb file it can load. If this is the case, please
11
+ make sure you are using Rosette CLI from the root of your Rails application. Do not hesitate to
12
+ raise an issue if you need further assistance.
13
+ ERR
14
+ exit 1
15
+ end
16
+
17
+ require 'rosette'
18
+
19
+ command = ARGV.shift
20
+ ARGV.clear
21
+
22
+ Rosette::CLI.run(command: command)
data/config/routes.rb CHANGED
@@ -1,2 +1,3 @@
1
1
  Rosette::Engine.routes.draw do
2
+ resources :translations, only: [:create]
2
3
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rosette
4
+ class CLI
5
+ COMMANDS = %w[add remove read].freeze
6
+
7
+ class << self
8
+ def run(command:)
9
+ return display_help unless command.in?(COMMANDS)
10
+
11
+ ask_user_for_key
12
+ send(command)
13
+ end
14
+
15
+ private
16
+
17
+ def read
18
+ Rosette.available_locales.each do |locale|
19
+ translation = Manager.read(locale, @key)
20
+ puts "Translation for #{locale} is: #{translation}"
21
+ end
22
+ end
23
+
24
+ def add
25
+ Rosette.available_locales.each do |locale|
26
+ puts "Please enter #{locale} translation:"
27
+ Manager.create(locale, @key, gets.chomp)
28
+ end
29
+ end
30
+
31
+ def remove
32
+ Rosette.available_locales.each do |locale|
33
+ Manager.delete(locale, @key)
34
+ end
35
+ end
36
+
37
+ def ask_user_for_key
38
+ puts 'Please provide the key to translation:'
39
+ @key = gets.chomp
40
+
41
+ normalize_key!
42
+ end
43
+
44
+ def normalize_key!
45
+ Rosette.available_locales.each do |locale|
46
+ @key.delete_prefix!("#{locale}.")
47
+ end
48
+ end
49
+
50
+ def display_help
51
+ puts <<~HELP
52
+ Usage: rosette [command]
53
+
54
+ Available commands:
55
+
56
+ read read translation from available locales
57
+ add add a translation to available locales
58
+ remove remove translation from available locales
59
+
60
+ For each command you must provide a key pointing to the translation.
61
+ It can begin with or without the locale. These are all valid keys:
62
+
63
+ fr.activemodel.errors.blank
64
+ en.activemodel.errors.blank
65
+ activemodel.errors.blank
66
+
67
+ HELP
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rosette
4
+ module Controller
5
+ extend ActiveSupport::Concern
6
+
7
+ included { rescue_from I18n::MissingTranslationData, with: :add_missing_translation_data }
8
+
9
+ private
10
+
11
+ def add_missing_translation_data(exception)
12
+ key = exception.message.sub(/translation missing:.+?\./, '')
13
+
14
+ render_rosette_new(key, request.fullpath)
15
+ end
16
+
17
+ def render_rosette_new(key, redirect_path)
18
+ render 'rosette/new', layout: 'rosette/application', locals: { key: key, redirect_path: redirect_path }
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rosette
2
4
  class Engine < ::Rails::Engine
3
5
  isolate_namespace Rosette
6
+
7
+ config.after_initialize do |app|
8
+ app.routes.prepend do
9
+ mount Rosette::Engine, at: '/rosette'
10
+ end
11
+ end
12
+
13
+ ActiveSupport.on_load(:action_controller_base) do
14
+ include Rosette::Controller
15
+ end
4
16
  end
5
17
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Manager
4
+ %w[create read delete].each do |verb|
5
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
6
+ def self.#{verb}(*args)
7
+ new(*args).send('#{verb}')
8
+ end
9
+ METHOD
10
+ end
11
+
12
+ private
13
+
14
+ def initialize(locale, key, translation = nil)
15
+ @file_path = Rails.root.join("config/locales/#{locale}.yml")
16
+ @keys = normalize "#{locale}.#{key}"
17
+ @translation = translation
18
+ end
19
+
20
+ def read
21
+ stored_translations.dig(*@keys)
22
+ end
23
+
24
+ def create
25
+ new_translation = @keys.reverse.inject(@translation) { |v, k| { k => v } }
26
+ updated_translations = deep_merge(stored_translations, new_translation)
27
+ persist(updated_translations)
28
+
29
+ Manager.normalize!
30
+ end
31
+
32
+ def delete
33
+ updated_translations = delete_translation(stored_translations, @keys)
34
+ persist(updated_translations)
35
+
36
+ Manager.normalize!
37
+ end
38
+
39
+ def delete_translation(translations, keys)
40
+ return translations.delete(keys.first) if keys.length == 1
41
+
42
+ head, *tail = keys
43
+ delete_translation(translations[head], tail)
44
+
45
+ translations
46
+ end
47
+
48
+ def persist(translations)
49
+ File.write(@file_path, translations.deep_stringify_keys.to_yaml)
50
+ end
51
+
52
+ def stored_translations
53
+ YAML.safe_load(File.read(@file_path), symbolize_names: true)
54
+ end
55
+
56
+ def deep_merge(hash, other_hash)
57
+ hash.merge(other_hash) do |_key, this_val, other_val|
58
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
59
+ deep_merge(this_val, other_val)
60
+ else
61
+ other_val
62
+ end
63
+ end
64
+ end
65
+
66
+ def normalize(key)
67
+ key.split('.').map(&:to_sym)
68
+ end
69
+
70
+ def self.normalize!
71
+ I18n::Tasks::CLI.start(['normalize']) if Rosette.normalize
72
+ end
73
+ end
@@ -1,3 +1,3 @@
1
1
  module Rosette
2
- VERSION = "0.1.1"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/rosette.rb CHANGED
@@ -1,6 +1,18 @@
1
- require "rosette/version"
1
+ # frozen_string_literal: true
2
+
3
+ require "rosette/cli"
4
+ require "rosette/controller"
2
5
  require "rosette/engine"
6
+ require "rosette/manager"
7
+ require "rosette/version"
8
+
9
+ require "i18n/tasks/cli"
10
+ require "yaml"
3
11
 
4
12
  module Rosette
5
- # Your code goes here...
13
+ mattr_accessor :normalize
14
+
15
+ def self.available_locales
16
+ Rails.configuration.i18n.available_locales.map(&:to_s)
17
+ end
6
18
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rosette
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Platteeuw
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-17 00:00:00.000000000 Z
11
+ date: 2023-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: i18n-tasks
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.12
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.12
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rails
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -41,7 +55,8 @@ dependencies:
41
55
  description: Add missing translations from the interface of your application
42
56
  email:
43
57
  - alexplatteeuw@gmail.com
44
- executables: []
58
+ executables:
59
+ - rosette
45
60
  extensions: []
46
61
  extra_rdoc_files: []
47
62
  files:
@@ -51,12 +66,19 @@ files:
51
66
  - app/assets/config/rosette_manifest.js
52
67
  - app/assets/stylesheets/rosette/application.css
53
68
  - app/controllers/rosette/application_controller.rb
69
+ - app/controllers/rosette/translations_controller.rb
54
70
  - app/helpers/rosette/application_helper.rb
55
71
  - app/models/rosette/application_record.rb
56
72
  - app/views/layouts/rosette/application.html.erb
73
+ - app/views/rosette/new.html.erb
74
+ - bin/rails
75
+ - bin/rosette
57
76
  - config/routes.rb
58
77
  - lib/rosette.rb
78
+ - lib/rosette/cli.rb
79
+ - lib/rosette/controller.rb
59
80
  - lib/rosette/engine.rb
81
+ - lib/rosette/manager.rb
60
82
  - lib/rosette/version.rb
61
83
  - lib/tasks/rosette_tasks.rake
62
84
  homepage: https://rubygems.org/gems/rosette