# encoding: UTF-8 module Rosette module Core # Configuration for a single repository. Instances of {RepoConfig} can # be configured to: # # * Extract phrases (via {#add_extractor}). Phrase extraction means # certain files (that you specify) will be monitored for changes and # processed. For example, you could specify that all files with a .yml # extension be monitored. When Rosette, using git, detects that any of # those files have changed, it will parse the files using an extractor # and store the phrases in the datastore. The Rosette project contains # a number of pre-built extractors. Visit github for a complete list: # https://github.com/rosette-proj. For example, the yaml extractor is # called rosette-extractor-yaml and is available at # https://github.com/rosette-proj/rosette-extractor-yaml. You would # need to add it to your Gemfile and require it before use. # # * Serialize phrases (via {#add_serializer}). Serializing phrases can be # thought of as the opposite of extracting them. Instead of parsing a # yaml file for example, serialization is the process of turning a # collection of foreign language translations into a big string of yaml # that can be written to a file. Usually serialization happens when # you're ready to export translations from Rosette. In the Rails world # for example, you'd export (or serialize) translations per locale and # store them as files in the config/locales directory. Spanish # translations would be exported to config/locales/es.yml and Japanese # translations to config/locales/ja.yml. The Rosette project contains # a number of pre-built serializers. Visit github for a complete list: # https://github.com/rosette-proj. For example, the yaml serializer is # called rosette-serializer-yaml and is available at # https://github.com/rosette-proj/rosette-serializer-yaml. You would # need to add it to your Gemfile and require it before use. # # * Pre-process phrases using {SerializerConfig#add_preprocessor}. # Serializers can also pre-process translations (see the example below). # Pre-processing is the concept of modifying a translation just before # it gets serialized. Examples include rosette-preprocessor-normalization, # which is capable of applying Unicode's text normalization algorithm # to translation text. See https://github.com/rosette-proj for a complete # list of pre-processors. # # * Interact with third-party libraries or services via integrations (see # the {#add_integration} method). Integrations are very general in that # they can be almost anything. For the most part however, integrations # serve as bridges to external APIs or libraries. For example, the # Rosette project currently contains an integration called # rosette-integration-smartling that's responsible for pushing and # pulling translations to/from the Smartling translation platform # (Smartling is a translation management system, or TMS). Since Rosette # is not a TMS (i.e. doesn't provide any GUI for entering translations), # you will need to use a third-party service like Smartling or build # your own TMS solution. Another example is rosette-integration-rollbar. # Rollbar is a third-party error reporting system. The Rollbar # integration not only adds a Rosette-style {ErrorReporter}, it also # hooks into a few places errors might happen, like Rosette::Server's # rack stack. # # @example # config = RepoConfig.new('my_repo') # .set_path('/path/to/my_repo/.git') # .set_description('My awesome repo') # .set_source_locale('en-US') # .add_locales(%w(pt-BR es-ES fr-FR ja-JP ko-KR)) # .add_extractor('yaml/rails') do |ext| # ext.match_file_extension('.yml').and( # ext.match_path('config/locales') # ) # end # .add_serializer('rails', format: 'yaml/rails') do |ser| # ser.add_preprocessor('normalization') do |pre| # pre.set_normalization_form(:nfc) # end # end # .add_integration('smartling') do |sm| # sm.set_api_options(smartling_api_key: 'fakefake', ... ) # sm.set_serializer('yaml/rails') # end # # @!attribute [r] name # @return [String] the name of the repository. # @!attribute [r] repo # @return [Repo] a {Repo} instance that can be used to perform git # operations on the local working copy of the associated git # repository. # @!attribute [r] locales # @return [Array] a list of the locales this repo supports. # @!attribute [r] hooks # @return [Hash>>] a hash of callbacks. The outer hash # contains the order while the inner hash contains the action. For # example, if the +hooks+ hash has been configured to do something # after commit, it might look like this: # { after: { commit: [] } } # @!attribute [r] description # @return [String] a description of the repository. # @!attribute [r] extractor_configs # @return [Array] a list of the currently configured # extractors. # @!attribute [r] serializer_configs # @return [Array] a list of the currently configured # serializers. # @!attribute [r] tms # @return [Rosette::Tms::Repository] a repository instance from the chosen # translation management system. class RepoConfig include Integrations::Integratable attr_reader :name, :rosette_config, :repo, :locales, :hooks, :description attr_reader :extractor_configs, :serializer_configs, :tms attr_reader :placeholder_regexes # Creates a new repo config object. # # @param [String] name The name of the repository. Usually matches the # name of the directory on disk, but that's not required. def initialize(name, rosette_config) @name = name @rosette_config = rosette_config @extractor_configs = [] @serializer_configs = [] @locales = [] @placeholder_regexes = [] @hooks = Hash.new do |h, key| h[key] = Hash.new do |h2, key2| h2[key2] = [] end end end # Sets the path to the repository's .git directory. # # @param [String] path The path to the repository's .git directory. # @return [void] def set_path(path) @repo = Repo.from_path(path) end # Sets the description of the repository. This is really just for # annotation purposes, the description isn't used by Rosette. # # @param [String] desc The description text. # @return [void] def set_description(desc) @description = desc end # Gets the path to the repository's .git directory. # # @return [String] def path repo.path if repo end # Gets the source locale (i.e. the locale all the source files are in). # Defaults to en-US. # # @return [Locale] the source locale. def source_locale @source_locale ||= Locale.parse('en-US', Locale::DEFAULT_FORMAT) end # Sets the source locale. # # @param [String] code The locale code. # @param [Symbol] format The format +locale+ is in. # @return [void] def set_source_locale(code, format = Locale::DEFAULT_FORMAT) @source_locale = Locale.parse(code, format) end # Set the TMS (translation management system). TMSs must contain a class # named +Repository+ that implements the [Rosette::Tms::Repository] # interface. # # @param [Const, String] tms The TMS to use. When this parameter is a # string, +use_tms+ will try to look up the corresponding +Tms+ # constant. If a constant is given instead, it's used without # modifications. In both cases, the +Tms+ constant will have +configure+ # called on it and is expected to yield a configuration object. # @param [Hash] options A hash of options passed to the TMS's # constructor. # @return [void] def use_tms(tms, options = {}) const = case tms when String if const = find_tms_const(tms) const else raise ArgumentError, "'#{tms}' couldn't be found." end when Class, Module tms else raise ArgumentError, "'#{tms}' must be a String, Class, or Module." end @tms = const.configure(rosette_config, self) do |configurator| yield configurator if block_given? end nil end # Adds an extractor to this repo. # # @param [String] extractor_id The id of the extractor you'd like to add. # @yield [config] yields the extractor config # @yieldparam config [ExtractorConfig] # @return [void] def add_extractor(extractor_id) klass = ExtractorId.resolve(extractor_id) extractor_configs << ExtractorConfig.new(extractor_id, klass).tap do |config| yield config if block_given? end end # Adds a serializer to this repo. # # @param [String] name A semantic name for this serializer. Means nothing # to Rosette, simply a way for you to label the serializer. # @param [Hash] options A hash of options containing the following entries: # * +format+: The id of the serializer, eg. "yaml/rails". # @yield [config] yields the serializer config # @yieldparam config [SerializerConfig] # @return [void] def add_serializer(name, options = {}) serializer_id = options[:format] klass = SerializerId.resolve(serializer_id) serializer_configs << SerializerConfig.new(name, klass, serializer_id).tap do |config| yield config if block_given? end end # Adds a locale to the list of locales this repo supports. # # @param [String] locale_code The locale you'd like to add. # @param [Symbol] format The format of +locale_code+. # @return [void] def add_locale(locale_code, format = Locale::DEFAULT_FORMAT) add_locales(locale_code) end # Adds multiple locales to the list of locales this repo supports. # # @param [Array] locale_codes The list of locales to add. # @param [Symbol] format The format of +locale_codes+. # @return [void] def add_locales(locale_codes, format = Locale::DEFAULT_FORMAT) @locales += Array(locale_codes).map do |locale_code| Locale.parse(locale_code, format) end end # Adds an after hook. You should pass a block to this method. The # block will be executed when the hook fires. # # @param [Symbol] action The action to hook. Currently the only # supported action is +:commit+. # @return [void] def after(action, &block) hooks[:after][action] << block end # Retrieves the extractor configs that match the given path. # # @param [String] path The path to match. # @return [Array] a list of the extractor configs that # were found to match +path+. def get_extractor_configs(path) extractor_configs.select do |config| config.matches?(path) end end # Retrieves the extractor config by either name or extractor id. # # @param [String] name_or_id The name or extractor id. # @return [nil, ExtractorConfig] the first matching extractor config. # Potentially returns +nil+ if no matching extractor config can be # found. def get_extractor_config(extractor_id) extractor_configs.find do |config| config.extractor_id == extractor_id end end # Retrieves the serializer config by either name or serializer id. # # @param [String] name_or_id The name or serializer id. # @return [nil, SerializerConfig] the first matching serializer config. # Potentially returns +nil+ if no matching serializer config can be # found. def get_serializer_config(name_or_id) found = serializer_configs.find do |config| config.name == name_or_id end found || serializer_configs.find do |config| config.serializer_id == name_or_id end end # Retrieves the locale object by locale code. # # @param [String] code The locale code to look for. # @param [Symbol] format The locale format +code+ is in. # @return [nil, Locale] The locale who's code matches +code+. Potentially # returns +nil+ if the locale can't be found. def get_locale(code, format = Locale::DEFAULT_FORMAT) locale_to_find = Locale.parse(code, format) locales.find { |locale| locale == locale_to_find } end # Adds a regex that matches a placeholder in translation text. For # example, Ruby placeholders often look like this "Hello %{name}!". # Some integrations rely on these regexes to detect and format # placeholders correctly. # # @param [Regexp] placeholder_regex The regex to add. # @return [void] def add_placeholder_regex(placeholder_regex) placeholder_regexes << placeholder_regex end protected def find_tms_const(name) const_str = "#{Rosette::Core::StringUtils.camelize(name)}Tms" if Rosette::Tms.const_defined?(const_str) Rosette::Tms.const_get(const_str) end end end end end