Skip to content

Commit

Permalink
Implement a built-in, dependency free localization adapter (#238)
Browse files Browse the repository at this point in the history
* Implement a built-in localization adapter

The built-in adapter is trivial and zero-deps.

This is thought to be the first step of a two step update.

**First step** (This one)

The gem ships a localization adapter that does not rely on i18n gem.
It has not feature parity because it can't interpolate variables.

The gem, when loaded, will check for `I18n` presence into the global
space. If it's not present, then it will use the buil-in adapter.
If it is present, then an alternative LightService::I18n::LocalizationAdapter
will be used.

LightService::I18n::LocalizationAdapter is exaclty the localization
adapter shipped previously.

The README is updated to reflect the new scenario.

Keep in mind that as it stands, i18n gem is required indirectly by
light-service as an activesupport dependency. This means that this
PR won't affect any implementation.

**Step two**

LightService::I18n::LocalizationAdapter and its concerning specs will
be moved into a new `light_service-i18n` gem (or `light_service-translation`).

The gem will stop to automatically choose the translation adapter, always
defaulting to the built-in one.  light_service-i18n will instead forcibly
configure itself as translation adapter.

Releasing this step will require to bump the major version and in release
notes will be noticed that in order to preserve previous i18n based
translation logic will require to add the new gem to the implementor's Gemfile.

Alternatively, a new branch (`zero-deps`, e.g.) will be created on the original
repository and the PR containing "step two" will be pointed to that
"bridge" branch. The major version bump could this way be delayed to the
future when generators will be migrated to dedicated gem and the current
inflector (activesupport) will be changed to something else.

* Update README.md

Co-authored-by: Attila Domokos <[email protected]>

* Update README.md

Co-authored-by: Attila Domokos <[email protected]>

Co-authored-by: Attila Domokos <[email protected]>
  • Loading branch information
alessandro-fazzi and adomokos authored Dec 6, 2022
1 parent 5ac824a commit bc5dcc0
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 99 deletions.
117 changes: 92 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,38 @@
LightService is a powerful and flexible service skeleton framework with an emphasis on simplicity

## Table of Contents
* [Why LightService?](#why-lightservice)
* [Getting Started](#getting-started)
* [Requirements](#requirements)
* [Installation](#installation)
* [Your first action](#your-first-action)
* [Your first organizer](#your-first-organizer)
* [Stopping the Series of Actions](#stopping-the-series-of-actions)
* [Failing the Context](#failing-the-context)
* [Skipping the Rest of the Actions](#skipping-the-rest-of-the-actions)
* [Benchmarking Actions with Around Advice](#benchmarking-actions-with-around-advice)
* [Before and After Action Hooks](#before-and-after-action-hooks)
* [Expects and Promises](#expects-and-promises)
* [Default values for optional Expected keys](#default-values-for-optional-expected-keys)
* [Key Aliases](#key-aliases)
* [Logging](#logging)
* [Error Codes](#error-codes)
* [Action Rollback](#action-rollback)
* [Localizing Messages](#localizing-messages)
* [Orchestrating Logic in Organizers](#orchestrating-logic-in-organizers)
* [ContextFactory for Faster Action Testing](#contextfactory-for-faster-action-testing)
* [Rails support](#rails-support)
* [Implementations in other languages](#other-implementations)
* [Contributing](#contributing)
- [Table of Contents](#table-of-contents)
- [Why LightService?](#why-lightservice)
- [Getting started](#getting-started)
- [Requirements](#requirements)
- [Installation](#installation)
- [Your first action](#your-first-action)
- [Your first organizer](#your-first-organizer)
- [Stopping the Series of Actions](#stopping-the-series-of-actions)
- [Failing the Context](#failing-the-context)
- [Skipping the rest of the actions](#skipping-the-rest-of-the-actions)
- [Benchmarking Actions with Around Advice](#benchmarking-actions-with-around-advice)
- [Before and After Action Hooks](#before-and-after-action-hooks)
- [Expects and Promises](#expects-and-promises)
- [Default values for optional Expected keys](#default-values-for-optional-expected-keys)
- [Key Aliases](#key-aliases)
- [Logging](#logging)
- [Error Codes](#error-codes)
- [Action Rollback](#action-rollback)
- [Localizing Messages](#localizing-messages)
- [Built-in localization adapter](#built-in-localization-adapter)
- [I18n localization adapter](#i18n-localization-adapter)
- [Custom localization adapter](#custom-localization-adapter)
- [Orchestrating Logic in Organizers](#orchestrating-logic-in-organizers)
- [ContextFactory for Faster Action Testing](#contextfactory-for-faster-action-testing)
- [Rails support](#rails-support)
- [Organizer generation](#organizer-generation)
- [Action generation](#action-generation)
- [Advanced action generation](#advanced-action-generation)
- [Other implementations](#other-implementations)
- [Contributing](#contributing)
- [Release Notes](#release-notes)
- [License](#license)

## Why LightService?

Expand Down Expand Up @@ -823,7 +832,61 @@ end
```
## Localizing Messages
By default LightService provides a mechanism for easily translating your error or success messages via I18n. You can also provide your own custom localization adapter if your application's logic is more complex than what is shown here.
### Built-in localization adapter
The built-in adapter simply uses a manually created dictionary to search for translations.
```ruby
# lib/light_service_translations.rb
LightService::LocalizationMap.instance[:en] = {
:foo_action => {
:light_service => {
:failures => {
:exceeded_api_limit => "API limit for service Foo reached. Please try again later."
},
:successes => {
:yay => "Yaaay!"
}
}
}
}
```
```ruby
class FooAction
extend LightService::Action
executed do |context|
unless service_call.success?
context.fail!(:exceeded_api_limit)
# The failure message used here equates to:
# LightService::LocalizationMap.instance[:en][:foo_action][:light_service][:failures][:exceeded_api_limit]
end
end
end
```
Nested classes will work too: `App::FooAction`, for example, would be translated to `app/foo_action` hash key.
`:en` is the default locale, but you can switch it whenever you want with
```ruby
LightService::Configuration.locale = :it
```
If you have `I18n` loaded in your project the default adapter will automatically be updated to use it.
But would you want to opt for the built-in localization adapter you can force it with
```ruby
LightService::Configuration.localization_adapter = LightService::LocalizationAdapter
```
### I18n localization adapter
If `I18n` is loaded into your project, LightService will automatically provide a mechanism for easily translating your error or success messages via `I18n`.
```ruby
class FooAction
Expand Down Expand Up @@ -881,13 +944,17 @@ module PaymentGateway
end
```
### Custom localization adapter
You can also provide your own custom localization adapter if your application's logic is more complex than what is shown here.
To provide your own custom adapter, use the configuration setting and subclass the default adapter LightService provides.
```ruby
LightService::Configuration.localization_adapter = MyLocalizer.new
# lib/my_localizer.rb
class MyLocalizer < LightService::LocalizationAdapter
class MyLocalizer < LightService::I18n::LocalizationAdapter
# I just want to change the default lookup path
# => "light_service.failures.payment_gateway/capture_funds"
Expand Down
2 changes: 2 additions & 0 deletions lib/light-service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
require 'light-service/errors'
require 'light-service/configuration'
require 'light-service/localization_adapter'
require 'light-service/localization_map'
require 'light-service/i18n/localization_adapter'
require 'light-service/context'
require 'light-service/context/key_verifier'
require 'light-service/organizer/scoped_reducable'
Expand Down
12 changes: 10 additions & 2 deletions lib/light-service/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@ module LightService
class Configuration
class << self
attr_accessor :capture_errors
attr_writer :logger, :localization_adapter
attr_writer :logger, :localization_adapter, :locale

def logger
@logger ||= _default_logger
end

def localization_adapter
@localization_adapter ||= LocalizationAdapter.new
@localization_adapter ||= if Module.const_defined?('I18n')
LightService::I18n::LocalizationAdapter.new
else
LocalizationAdapter.new
end
end

def locale
@locale ||= :en
end

private
Expand Down
46 changes: 46 additions & 0 deletions lib/light-service/i18n/localization_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module LightService
module I18n
class LocalizationAdapter
def failure(message_or_key, action_class, i18n_options = {})
find_translated_message(message_or_key,
action_class,
i18n_options,
:type => :failure)
end

def success(message_or_key, action_class, i18n_options = {})
find_translated_message(message_or_key,
action_class,
i18n_options,
:type => :success)
end

private

def find_translated_message(message_or_key,
action_class,
i18n_options,
type)
if message_or_key.is_a?(Symbol)
i18n_options.merge!(type)
translate(message_or_key, action_class, i18n_options)
else
message_or_key
end
end

def translate(key, action_class, options = {})
type = options.delete(:type)

scope = i18n_scope_from_class(action_class, type)
options[:scope] = scope

::I18n.t(key, **options)
end

def i18n_scope_from_class(action_class, type)
"#{action_class.name.underscore}.light_service.#{type.to_s.pluralize}"
end
end
end
end
41 changes: 14 additions & 27 deletions lib/light-service/localization_adapter.rb
Original file line number Diff line number Diff line change
@@ -1,44 +1,31 @@
module LightService
class LocalizationAdapter
def failure(message_or_key, action_class, i18n_options = {})
def failure(message_or_key, action_class)
find_translated_message(message_or_key,
action_class,
i18n_options,
:type => :failure)
action_class.to_s.underscore,
:failures)
end

def success(message_or_key, action_class, i18n_options = {})
def success(message_or_key, action_class)
find_translated_message(message_or_key,
action_class,
i18n_options,
:type => :success)
action_class.to_s.underscore,
:successes)
end

private

def find_translated_message(message_or_key,
action_class,
i18n_options,
type)
def find_translated_message(message_or_key, action_class, type)
if message_or_key.is_a?(Symbol)
i18n_options.merge!(type)
translate(message_or_key, action_class, i18n_options)
LightService::LocalizationMap.instance.dig(
LightService::Configuration.locale,
action_class.to_sym,
:light_service,
type,
message_or_key
)
else
message_or_key
end
end

def translate(key, action_class, options = {})
type = options.delete(:type)

scope = i18n_scope_from_class(action_class, type)
options[:scope] = scope

I18n.t(key, **options)
end

def i18n_scope_from_class(action_class, type)
"#{action_class.name.underscore}.light_service.#{type.to_s.pluralize}"
end
end
end
7 changes: 7 additions & 0 deletions lib/light-service/localization_map.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require 'singleton'

module LightService
class LocalizationMap < Hash
include ::Singleton
end
end
14 changes: 7 additions & 7 deletions spec/acceptance/message_localization_spec.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
require "spec_helper"
require "test_doubles"

class TestsLocalizationAdapter
class TestsI18nLocalizationAdapter
extend LightService::Organizer

def self.call(pass_or_fail, message_or_key, i18n_options = {})
with(
:pass_or_fail => pass_or_fail,
:message_or_key => message_or_key,
:i18n_options => i18n_options
).reduce(TestsLocalizationInvocationOptionsAction)
).reduce(TestsI18nLocalizationInvocationOptionsAction)
end
end

class TestsLocalizationInvocationOptionsAction
class TestsI18nLocalizationInvocationOptionsAction
extend LightService::Action
expects :pass_or_fail, :message_or_key, :i18n_options

Expand All @@ -27,18 +27,18 @@ class TestsLocalizationInvocationOptionsAction
end

def pass_with(message_or_key, i18n_options = {})
TestsLocalizationAdapter.call(true, message_or_key, i18n_options)
TestsI18nLocalizationAdapter.call(true, message_or_key, i18n_options)
end

def fail_with(message_or_key, i18n_options = {})
TestsLocalizationAdapter.call(false, message_or_key, i18n_options)
TestsI18nLocalizationAdapter.call(false, message_or_key, i18n_options)
end

describe "Localization Adapter" do
describe "I18n Localization Adapter" do
before do
I18n.backend.store_translations(
:en,
:tests_localization_invocation_options_action =>
:tests_i18n_localization_invocation_options_action =>
{
:light_service => {
:failures => {
Expand Down
Loading

0 comments on commit bc5dcc0

Please sign in to comment.