# frozen_string_literal: true require 'i18n' require 'request_store' require 'mobility/version' =begin Mobility is a gem for storing and retrieving localized data through attributes on a class. There are two ways to translate attributes on a class, both of which are variations on the same basic mechanism. The first and most common way is to extend the `Mobility` module, which adds a class method +translates+. Translated attributes can then be defined like this: class Post extend Mobility translates :title, backend: :key_value end Behind the scenes, +translates+ simply creates an instance of +Mobility.translations_class+, passes it whatever arguments are passed to +translates+, and includes the instance (which is a module) into the class. So the above example is equivalent to: class Post Mobility.translations_class.new(:title, backend: :key_value) end `Mobility.translations_class` is a subclass of `Mobility::Translations` created when `Mobility.configure` is called to configure Mobility. In fact, when you call `Mobility.configure`, it is the subclass of `Mobility::Translations` which is passed to the block as `config`. Plugins and plugin configuration is all applied to the same `Mobility.translations_class`. There is another way to use Mobility, which is to create your own subclass or subclasses of +Mobility::Translations+ and include them explicitly, without using +translates+. For example: class Translations < Mobility::Translations plugins do backend :key_value # ... end end class Post include Translations.new(:title) end This usage might be handy if, for example, you want to have more complex configuration, where some models use some plugins while others do not. Since `Mobility::Translations` is a class like any other, you can subclass it and define plugins specifically on the subclass which are not present on its parent: class TranslationsWithFallbacks < Translations plugins do fallbacks end end class Comment include TranslationsWithFallbacks.new(:author) end In this case, +Comment+ uses +TranslationsWithFallbacks+ and thus has the fallbacks plugin, whereas +Post+ uses +Translations+ which does not have that plugin enabled. =end module Mobility # A generic exception used by Mobility. class Error < StandardError end require "mobility/backend" require "mobility/backends" require "mobility/plugin" require "mobility/plugins" require "mobility/translations" # General error for version compatibility conflicts class VersionNotSupportedError < ArgumentError; end CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/ private_constant :CALL_COMPILABLE_REGEXP require "rails/generators/mobility/generators" if defined?(Rails) class << self def extended(model_class) def model_class.translates(*args, **options) include Mobility.translations_class.new(*args, **options) end end # Extends model with this class so that +include Mobility+ is equivalent to # +extend Mobility+ (but +extend+ is preferred). # @param model_class def included(model_class) model_class.extend self end # Alias to default backend defined on *translations_class+. # @return [Symbol,Class] def default_backend translations_class.defaults[:backend] end # Configure Mobility # @yield [Mobility::Translations] def configure(&block) translates_with(Class.new(Translations)) unless @translations_class if block.arity == 0 translations_class.instance_exec(&block) else yield translations_class end end def translates_with(pluggable) raise ArgumentError, "translations class must be a subclass of Module." unless Module === pluggable @translations_class = pluggable end def translations_class @translations_class || raise(Error, "Mobility has not been configured. "\ "Configure with Mobility.configure, or assign a translations class with Mobility.translates_with()") end def reset_translations_class @translations_class = nil end # @!group Locale Accessors # @return [Symbol] Mobility locale def locale read_locale || I18n.locale end # Sets Mobility locale # @param [Symbol] locale Locale to set # @raise [InvalidLocale] if locale is nil or not in # +Mobility.available_locales+ (if +I18n.enforce_available_locales+ is +true+) # @return [Symbol] Locale def locale=(locale) set_locale(locale) end # Sets Mobility locale around block # @param [Symbol] locale Locale to set in block # @yield [Symbol] Locale def with_locale(locale) previous_locale = read_locale begin set_locale(locale) yield(locale) ensure set_locale(previous_locale) end end # @!endgroup # @return [RequestStore] Request store def storage RequestStore.store end # Return normalized locale # @param [String,Symbol] locale # @return [String] Normalized locale # @example # Mobility.normalize_locale(:ja) # #=> "ja" # Mobility.normalize_locale("pt-BR") # #=> "pt_br" def normalize_locale(locale = Mobility.locale) "#{locale.to_s.downcase.tr("-", "_")}" end alias_method :normalized_locale, :normalize_locale # Return normalized locale accessor name # @param [String,Symbol] attribute # @param [String,Symbol] locale # @return [String] Normalized locale accessor name # @raise [ArgumentError] if generated accessor has an invalid format # @example # Mobility.normalize_locale_accessor(:foo, :ja) # #=> "foo_ja" # Mobility.normalize_locale_accessor(:bar, "pt-BR") # #=> "bar_pt_br" def normalize_locale_accessor(attribute, locale = Mobility.locale) "#{attribute}_#{normalize_locale(locale)}".tap do |accessor| unless CALL_COMPILABLE_REGEXP.match(accessor) raise ArgumentError, "#{accessor.inspect} is not a valid accessor" end end end # Raises InvalidLocale exception if the locale passed in is present but not available. # @param [String,Symbol] locale # @raise [InvalidLocale] if locale is present but not available def enforce_available_locales!(locale) raise Mobility::InvalidLocale.new(locale) unless (locale.nil? || available_locales.include?(locale.to_sym)) end # Returns available locales. Defaults to I18n.available_locales, but will # use Rails.application.config.i18n.available_locales if Rails is loaded # and config is non-nil. # @return [Array] Available locales # @note The special case for Rails is necessary due to the fact that Rails # may load the model before setting +I18n.available_locales+. If we # simply default to +I18n.available_locales+, we may define many more # methods (in LocaleAccessors) than is really necessary. def available_locales if defined?(Rails) && Rails.application Rails.application.config.i18n.available_locales || I18n.available_locales else I18n.available_locales end end protected def read_locale storage[:mobility_locale] end def set_locale(locale) locale = locale.to_sym if locale enforce_available_locales!(locale) if I18n.enforce_available_locales storage[:mobility_locale] = locale end end class InvalidLocale < I18n::InvalidLocale; end class NotImplementedError < StandardError; end end