Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[![CI](https://github.com/virolea/rosetta/actions/workflows/ci.yml/badge.svg)](https://github.com/virolea/rosetta/actions/workflows/ci.yml)

# Rosetta

Rosetta is a Rails engine proviving a full-fledged internationalization (i18n) solution for your Rails application. It is designed with the following principles in mind:
Expand Down
3 changes: 1 addition & 2 deletions app/controllers/concerns/rosetta/locale_scoped.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ module LocaleScoped
extend ActiveSupport::Concern

included do
around_action :set_locale
before_action :set_locale
end

private

def set_locale(&action)
@locale = Locale.find(params[:locale_id])
Rosetta.with_locale(@locale, &action)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class Locales::Translations::MissingController < Locales::TranslationsController
private

def scope
super.where.missing(:translation_in_current_locale)
TranslationKey.with_missing_translation(@locale)
end
end
end
2 changes: 1 addition & 1 deletion app/controllers/rosetta/locales/translations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def index
private

def scope
TranslationKey.includes(:translation_in_current_locale)
TranslationKey.with_translation(@locale)
end
end
end
15 changes: 3 additions & 12 deletions app/controllers/rosetta/translations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,12 @@ class TranslationsController < ApplicationController
include LocaleScoped

before_action :set_translation_key
before_action :set_translation

def edit
end

def update
if translation_params[:value].blank?
@translation_key.translation_in_current_locale = nil
else
@translation.update(translation_params)
end
@translation_key.update(translation_key_params)

render partial: "rosetta/locales/translations/translation_key", locals: { translation_key: @translation_key }
end
Expand All @@ -24,12 +19,8 @@ def set_translation_key
@translation_key = TranslationKey.find(params[:translation_key_id])
end

def set_translation
@translation = @translation_key.translation_in_current_locale || @translation_key.build_translation_in_current_locale
end

def translation_params
params.require(:translation).permit(:value)
def translation_key_params
params.require(:translation_key).permit(:"value_#{@locale.code}")
end
end
end
11 changes: 11 additions & 0 deletions app/models/rosetta/locale.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Rosetta
class Locale < ApplicationRecord
class_attribute :registered_classes_for_translations, default: []

CODE_FORMAT = /\A[a-zA-Z]+(-[a-zA-Z]+)?\z/

validates :name, :code, presence: true
Expand All @@ -8,6 +10,8 @@ class Locale < ApplicationRecord

has_many :translations, dependent: :destroy

after_create_commit :notify_translated_models

class << self
def available_locales
all
Expand All @@ -24,6 +28,13 @@ def default_locale
def default_locale=(locale)
@default_locale = locale
end
def register_class_for_translation(klass)
registered_classes_for_translations << klass
end
end

def notify_translated_models
registered_classes_for_translations.each { |klass| klass.translated_in(self) }
end
end
end
6 changes: 3 additions & 3 deletions app/models/rosetta/translation_key.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module Rosetta
class TranslationKey < ApplicationRecord
has_many :translations, dependent: :destroy
# Note: Learn about the design decisions behind this: https://github.com/virolea/rosetta/issues/3
has_one :translation_in_current_locale, -> { where(locale_id: Rosetta.locale.id) }, class_name: "Translation", dependent: :destroy
include Translated

translated_in_all_locales

def self.create_later(value)
AutodiscoveryJob.perform_later(value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<%= tab_link_to locale_translations_missing_index_path(@locale) do %>
Missing
<span class="ml-3 hidden md:inline-block rounded-full px-2.5 py-0.5 text-xs font-medium bg-gray-100 text-gray-900 group-[.active]:bg-indigo-100 group-[.active]:text-indigo-600">
<%= Rosetta::TranslationKey.where.missing(:translation_in_current_locale).size %>
<%= Rosetta::TranslationKey.with_missing_translation(@locale).size %>
</span>
<% end %>
</nav>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
</td>
<td class="px-3 py-4 text-sm text-gray-900 group relative">
<%= turbo_frame_tag dom_id(translation_key) do %>
<%= translation_key.translation_in_current_locale&.value %>
<%= translation_key.public_send("#{@locale.code}_translation")&.value %>

<div class="hidden group-hover:flex absolute w-full h-full top-0 left-0 bg-indigo-50 bg-opacity-50 pr-4 items-center justify-end">
<%= link_to "edit", edit_translation_key_translation_path(translation_key, locale_id: @locale.id), class: "text-indigo-600 hover:text-indigo-900 font-medium" %>
Expand Down
6 changes: 3 additions & 3 deletions app/views/rosetta/translations/edit.html.erb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<%= turbo_frame_tag dom_id(@translation_key) do %>
<%= form_with model: @translation, url: translation_key_translation_path(@translation_key), method: :patch, class: "relative" do |f| %>
<%= form_with model: @translation_key, url: translation_key_translation_path(@translation_key), method: :patch, class: "relative" do |f| %>
<%= hidden_field_tag :locale_id, @locale.id %>

<div class="overflow-hidden rounded-lg shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-indigo-600">
<%= f.label :value, "Translation", class: "sr-only" %>
<%= f.text_area :value, row: 3, autofocus: true, placeholder: "Enter your translation", class: "block w-full resize-none border-0 bg-transparent py-1.5 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6" %>
<%= f.label :"value_#{@locale.code}", "Translation", class: "sr-only" %>
<%= f.text_area :"value_#{@locale.code}", row: 3, autofocus: true, placeholder: "Enter your translation", class: "block w-full resize-none border-0 bg-transparent py-1.5 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6" %>

<div class="py-2" aria-hidden="true">
<div class="py-px">
Expand Down
4 changes: 4 additions & 0 deletions lib/rosetta-rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
require "rosetta/store"
require "rosetta/configuration"

require "rosetta/translated"
require "rosetta/translated/create"
require "rosetta/translated/delete"

module Rosetta
module Base
def locale
Expand Down
4 changes: 2 additions & 2 deletions lib/rosetta/store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ def translations
def load_translations
loaded_translations = Rosetta.with_locale(@locale) do
TranslationKey
.includes(:translation_in_current_locale)
.with_translation(@locale)
.map do |translation_key|
[ translation_key.value, translation_key.translation_in_current_locale&.value ]
[ translation_key.value, translation_key.public_send(:"#{@locale.code}_translation")&.value ]
end.to_h
end

Expand Down
52 changes: 52 additions & 0 deletions lib/rosetta/translated.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module Rosetta
module Translated
extend ActiveSupport::Concern

included do
has_many :translations, dependent: :destroy
end

class_methods do
def translated_in_all_locales
Locale.all.each do |locale|
translated_in(locale)
end

Locale.register_class_for_translation(self)
end

def translated_in(locale)
has_one :"#{locale.code}_translation", -> { where(locale_id: locale.id) }, class_name: "Translation", dependent: :destroy

scope :with_translation, ->(locale) { includes(:"#{locale.code}_translation") }
scope :with_missing_translation, ->(locale) { with_translation(locale).where.missing(:"#{locale.code}_translation") }

define_method("value_#{locale.code}") do
if translation_changes[locale.code]
translation_changes[locale.code].value
else
public_send(:"#{locale.code}_translation")&.value
end
end

define_method("value_#{locale.code}=") do |locale_value|
translation_changes[locale.code] = if locale_value.blank?
Rosetta::Translated::Delete.new(self, locale)
else
Rosetta::Translated::Create.new(self, locale, locale_value)
end
end

after_save { translation_changes[locale.code]&.save }
end
end

def translation_changes
@translation_changes ||= {}
end

def reload(*)
super.tap { @translation_changes = nil }
end
end
end
20 changes: 20 additions & 0 deletions lib/rosetta/translated/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Rosetta
class Translated::Create
attr_reader :value

def initialize(record, locale, value)
@record = record
@locale = locale
@value = value
end

def save
translation.value = @value
@record.public_send(:"#{@locale.code}_translation=", translation)
end

def translation
@translation ||= @record.public_send(:"build_#{@locale.code}_translation")
end
end
end
15 changes: 15 additions & 0 deletions lib/rosetta/translated/delete.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Rosetta
class Translated::Delete
attr_reader :value

def initialize(record, locale)
@record = record
@locale = locale
@value = nil
end

def save
@record.public_send(:"#{@locale.code}_translation=", nil)
end
end
end
6 changes: 3 additions & 3 deletions test/controllers/rosetta/translations_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class TranslationsControllerTest < ActionDispatch::IntegrationTest

test "add a new translation" do
assert_difference("Translation.count", 1) do
patch translation_key_translation_path(@key), params: { locale_id: @locale.id, translation: { value: "Au revoir" } }
patch translation_key_translation_path(@key), params: { locale_id: @locale.id, translation_key: { value_fr: "Au revoir" } }
end

assert_equal "Au revoir", Translation.last.value
Expand All @@ -28,7 +28,7 @@ class TranslationsControllerTest < ActionDispatch::IntegrationTest
Translation.create!(locale: @locale, translation_key: @key, value: "Salut")

assert_no_difference("Translation.count") do
patch translation_key_translation_path(@key), params: { locale_id: @locale.id, translation: { value: "Bonjour" } }
patch translation_key_translation_path(@key), params: { locale_id: @locale.id, translation_key: { value_fr: "Bonjour" } }
end

assert_equal "Bonjour", Translation.last.value
Expand All @@ -40,7 +40,7 @@ class TranslationsControllerTest < ActionDispatch::IntegrationTest
Translation.create!(locale: @locale, translation_key: @key, value: "Salut")

assert_difference("Translation.count", -1) do
patch translation_key_translation_path(@key), params: { locale_id: @locale.id, translation: { value: "" } }
patch translation_key_translation_path(@key), params: { locale_id: @locale.id, translation_key: { value_fr: "" } }
end

assert_response :success
Expand Down
2 changes: 1 addition & 1 deletion test/integration/translations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class TranslationsTest < ActionDispatch::IntegrationTest
key = Rosetta::TranslationKey.create(value: "Available locales")

# Create the translation
patch rosetta.translation_key_translation_path(key), params: { locale_id: locale.id, translation: { value: "Langues disponibles" } }
patch rosetta.translation_key_translation_path(key), params: { locale_id: locale.id, translation_key: { value_fr: "Langues disponibles" } }

# Deploy the changes
post rosetta.locale_deploys_path(locale)
Expand Down
32 changes: 32 additions & 0 deletions test/lib/rosetta/translated_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require "test_helper"

class Rosetta::TranslatedTest < ActiveSupport::TestCase
include ActiveJob::TestHelper

test "setting a new translation" do
translation_key = rosetta_translation_keys(:goodbye)
translation_key.update(value_fr: "au revoir")
translation_key.reload

assert_equal "au revoir", translation_key.value_fr
assert_not_nil translation_key.fr_translation
end

test "setting a translation to blank removes the translation" do
translation_key = rosetta_translation_keys(:hello)

translation_key.update(value_fr: "")
translation_key.reload

assert_nil translation_key.value_fr
assert_nil translation_key.fr_translation
end

test "setting a translation without saving returns the updated translation" do
translation_key = rosetta_translation_keys(:hello)
translation_key.value_fr = "salut"

assert_equal "salut", translation_key.value_fr
assert_equal "bonjour", translation_key.reload.value_fr
end
end
11 changes: 11 additions & 0 deletions test/models/rosetta/locale_test.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "test_helper"
require "minitest/mock"

class Rosetta::LocaleTest < ActiveSupport::TestCase
test "valid locale" do
Expand Down Expand Up @@ -48,4 +49,14 @@ class Rosetta::LocaleTest < ActiveSupport::TestCase
assert_equal "en", default_locale.code
assert default_locale.default?
end

test "creating a new locale notifies translated models" do
translated_model = Minitest::Mock.new
translated_model.expect(:translated_in, nil, [ Rosetta::Locale ])

Rosetta::Locale.register_class_for_translation(translated_model)
Rosetta::Locale.create(name: "Italian", code: "it")

assert translated_model.verify
end
end
2 changes: 2 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
class ActiveSupport::TestCase
# Provide a clean slate for each test:
# - Unset the default locale
# - Reset the registered classes for translations
# - Reload all locale stores
# - Set the locale to the default locale
setup do
Rosetta::Locale.default_locale = nil
Rosetta::Locale.registered_classes_for_translations = []
Rosetta::Store.locale_stores.each { |code, store| store.reload! }
Rosetta.locale = :en
end
Expand Down