├── .gitignore
├── Gemfile
├── lib
├── gettext_i18n_rails
│ ├── version.rb
│ ├── active_model.rb
│ ├── active_model
│ │ ├── name.rb
│ │ └── translation.rb
│ ├── active_record.rb
│ ├── slim_parser.rb
│ ├── gettext_hooks.rb
│ ├── string_interpolate_fix.rb
│ ├── i18n_hacks.rb
│ ├── html_safe_translations.rb
│ ├── haml_parser.rb
│ ├── action_controller.rb
│ ├── base_parser.rb
│ ├── railtie.rb
│ ├── backend.rb
│ ├── tasks.rb
│ └── model_attributes_finder.rb
├── tasks
│ └── gettext_rails_i18n.rake
└── gettext_i18n_rails.rb
├── gemfiles
├── rails72.gemfile
├── rails80.gemfile
├── rails72.gemfile.lock
└── rails80.gemfile.lock
├── CHANGELOG.md
├── Rakefile
├── spec
├── gettext_i18n_rails
│ ├── railtie_spec.rb
│ ├── active_model
│ │ └── name_spec.rb
│ ├── string_interpolate_fix_spec.rb
│ ├── model_attributes_finder_spec.rb
│ ├── slim_parser_spec.rb
│ ├── action_controller_spec.rb
│ ├── active_record_spec.rb
│ ├── haml_parser_spec.rb
│ └── backend_spec.rb
├── spec_helper.rb
└── gettext_i18n_rails_spec.rb
├── .github
└── workflows
│ └── test.yml
├── gettext_i18n_rails.gemspec
├── MIT-LICENSE.txt
├── Gemfile.lock
└── Readme.md
/.gitignore:
--------------------------------------------------------------------------------
1 | pkg/*.gem
2 | .ruby-version
3 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | gemspec
3 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/version.rb:
--------------------------------------------------------------------------------
1 | module GettextI18nRails
2 | Version = VERSION = '2.1.0'
3 | end
4 |
--------------------------------------------------------------------------------
/lib/tasks/gettext_rails_i18n.rake:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), "/../gettext_i18n_rails/tasks")
2 |
--------------------------------------------------------------------------------
/gemfiles/rails72.gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec :path => "../"
4 |
5 | gem "rails", "~> 7.2.0"
6 |
--------------------------------------------------------------------------------
/gemfiles/rails80.gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec :path => "../"
4 |
5 | gem "rails", "~> 8.0"
6 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/active_model.rb:
--------------------------------------------------------------------------------
1 | require 'gettext_i18n_rails/active_model/name'
2 | require 'gettext_i18n_rails/active_model/translation'
3 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/active_model/name.rb:
--------------------------------------------------------------------------------
1 | module ActiveModel
2 | Name.class_eval do
3 | def human(options={})
4 | human_name = @klass.humanize_class_name
5 |
6 | if count = options[:count]
7 | n_(human_name, human_name.pluralize, count)
8 | else
9 | _(human_name)
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/active_record.rb:
--------------------------------------------------------------------------------
1 | require 'gettext_i18n_rails/active_model/translation'
2 |
3 | class ActiveRecord::Base
4 | extend ActiveModel::Translation
5 |
6 | def self.human_attribute_name(*args)
7 | super(*args)
8 | end
9 |
10 | # method deprecated in Rails 3.1
11 | def self.human_name(*args)
12 | _(self.humanize_class_name)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/slim_parser.rb:
--------------------------------------------------------------------------------
1 | require 'gettext_i18n_rails/base_parser'
2 |
3 | module GettextI18nRails
4 | class SlimParser < BaseParser
5 | def self.extension
6 | "slim"
7 | end
8 |
9 | def self.convert_to_code(text)
10 | Slim::Engine.new.call(text)
11 | end
12 | end
13 | end
14 |
15 | GettextI18nRails::GettextHooks.add_parser GettextI18nRails::SlimParser
16 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/gettext_hooks.rb:
--------------------------------------------------------------------------------
1 | module GettextI18nRails
2 | module GettextHooks
3 | # shorter call / maybe the interface changes again ...
4 | def self.add_parser(parser)
5 | xgettext.add_parser(parser)
6 | end
7 |
8 | def self.xgettext
9 | @xgettext ||= begin
10 | require 'gettext/tools/xgettext'
11 | GetText::Tools::XGetText
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Unreleased
4 |
5 | ## 2.1.0
6 |
7 | - Add automatic reloading of .po and .mo files in development mode
8 |
9 | ## 2.0.0
10 |
11 | - change how model attributes are looked up (class first, then sti root)
12 | - drop support for old rubies
13 |
14 | ## v1.13.0
15 |
16 | - Use subclasses instead of direct_descendants on rails 7 and above
17 |
18 | ## v1.12.0
19 |
20 | - drop support for gettext < 3
21 | - improve haml and slim parsing
22 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 | require 'bundler/gem_tasks'
3 |
4 | require 'bump/tasks'
5 | Bump.replace_in_default = Dir["gemfiles/*.gemfile.lock"]
6 |
7 | task :spec do
8 | sh "rspec spec"
9 | end
10 |
11 | task :default => "spec"
12 |
13 | desc "bundle all gemfiles [EXTRA=]"
14 | task :bundle_all do
15 | extra = ENV["EXTRA"] || "install"
16 | gemfiles = (["Gemfile"] + Dir["gemfiles/*.gemfile"])
17 | gemfiles.each do |gemfile|
18 | Bundler.with_unbundled_env do
19 | sh "BUNDLE_GEMFILE=#{gemfile} bundle #{extra}"
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/gettext_i18n_rails/railtie_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe GettextI18nRails::Railtie do
4 | describe 'auto-reload configuration' do
5 | it 'can be set to true or false' do
6 | config = GettextI18nRails::Railtie.config.gettext_i18n_rails
7 | config.auto_reload = true
8 | config.auto_reload.should == true
9 | end
10 |
11 | it 'can be disabled' do
12 | config = GettextI18nRails::Railtie.config.gettext_i18n_rails
13 | config.auto_reload = false
14 | config.auto_reload.should == false
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/string_interpolate_fix.rb:
--------------------------------------------------------------------------------
1 | needed = "".respond_to?(:html_safe) and
2 | (
3 | "".html_safe % {:x => '
'} == '
' or
4 | not ("".html_safe % {:x=>'a'}).html_safe?
5 | )
6 |
7 | if needed
8 | class String
9 | alias :interpolate_without_html_safe :%
10 |
11 | def %(*args)
12 | if args.first.is_a?(Hash) and html_safe?
13 | safe_replacement = Hash[args.first.map{|k,v| [k,ERB::Util.h(v)] }]
14 | interpolate_without_html_safe(safe_replacement).html_safe
15 | else
16 | interpolate_without_html_safe(*args).dup # make sure its not html_safe
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/i18n_hacks.rb:
--------------------------------------------------------------------------------
1 | I18n::Config # autoload
2 |
3 | module I18n
4 | class Config
5 | def locale
6 | FastGettext.locale.gsub("_","-").to_sym
7 | end
8 |
9 | def locale=(new_locale)
10 | FastGettext.locale=(new_locale)
11 | end
12 | end
13 |
14 | # backport I18n.with_locale if it does not exist
15 | # Executes block with given I18n.locale set.
16 | def self.with_locale(tmp_locale = nil)
17 | if tmp_locale
18 | current_locale = self.locale
19 | self.locale = tmp_locale
20 | end
21 | yield
22 | ensure
23 | self.locale = current_locale if tmp_locale
24 | end unless defined? I18n.with_locale
25 | end
26 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/html_safe_translations.rb:
--------------------------------------------------------------------------------
1 | module GettextI18nRails
2 | mattr_accessor :translations_are_html_safe
3 |
4 | module HtmlSafeTranslations
5 | # also make available on class methods
6 | def self.included(base)
7 | base.extend self
8 | end
9 |
10 | def _(*args)
11 | html_safe_if_wanted super
12 | end
13 |
14 | def n_(*args)
15 | html_safe_if_wanted super
16 | end
17 |
18 | def s_(*args)
19 | html_safe_if_wanted super
20 | end
21 |
22 | private
23 |
24 | def html_safe_if_wanted(text)
25 | return text unless GettextI18nRails.translations_are_html_safe
26 | text.to_s.html_safe
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/haml_parser.rb:
--------------------------------------------------------------------------------
1 | require 'gettext_i18n_rails/base_parser'
2 |
3 | module GettextI18nRails
4 | class HamlParser < BaseParser
5 | def self.extension
6 | "haml"
7 | end
8 |
9 | def self.convert_to_code(text)
10 | case @library_loaded
11 | when "haml"
12 | if Haml::VERSION.split('.').first.to_i <= 5
13 | Haml::Engine.new(text).precompiled()
14 | else
15 | Haml::Engine.new.call(text)
16 | end
17 | when "hamlit"
18 | Hamlit::Engine.new.call(text)
19 | end
20 | end
21 |
22 | def self.libraries
23 | ["haml", "hamlit"]
24 | end
25 | end
26 | end
27 |
28 | GettextI18nRails::GettextHooks.add_parser GettextI18nRails::HamlParser
29 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/action_controller.rb:
--------------------------------------------------------------------------------
1 | # Autoloading in initializers is deprecated on rails 6.0. This delays initialization using the on_load
2 | # hooks, but does not change behaviour for existing rails versions.
3 | path_controller = ->() {
4 | class ::ActionController::Base
5 | def set_gettext_locale
6 | requested_locale = params[:locale] || session[:locale] || cookies[:locale] || request.env['HTTP_ACCEPT_LANGUAGE'] || I18n.default_locale
7 | locale = FastGettext.set_locale(requested_locale)
8 | session[:locale] = locale
9 | I18n.locale = locale # some weird overwriting in action-controller makes this necessary ... see I18nProxy
10 | end
11 | end
12 | }
13 | ActiveSupport.on_load(:action_controller_base) do
14 | path_controller.call
15 | end
16 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | - push
4 | - pull_request
5 | jobs:
6 | run:
7 | strategy:
8 | fail-fast: false
9 | matrix:
10 | gemfile:
11 | - gemfiles/rails72.gemfile
12 | - gemfiles/rails80.gemfile
13 | ruby-version:
14 | - "3.2"
15 | - "3.3"
16 | - "3.4"
17 | runs-on:
18 | - ubuntu-latest
19 | name: ${{ matrix.ruby-version}} on ${{ matrix.runs-on }} with ${{ matrix.gemfile }}
20 | runs-on: ${{ matrix.runs-on }}
21 | env:
22 | BUNDLE_GEMFILE: ${{ matrix.gemfile }}
23 | steps:
24 | - uses: actions/checkout@master
25 | - uses: ruby/setup-ruby@v1
26 | with:
27 | ruby-version: ${{ matrix.ruby-version }}
28 | bundler-cache: true
29 | - run: bundle exec rake
30 |
--------------------------------------------------------------------------------
/spec/gettext_i18n_rails/active_model/name_spec.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require "spec_helper"
3 |
4 | if ActiveRecord::VERSION::MAJOR >= 3
5 | require "gettext_i18n_rails/active_model/name"
6 |
7 | describe ActiveModel::Name do
8 | before do
9 | FastGettext.reload!
10 | end
11 |
12 | describe '#human' do
13 | it "is translated through FastGettext" do
14 | name = ActiveModel::Name.new(CarSeat)
15 | name.should_receive(:_).with('Car seat').and_return('Autositz')
16 | name.human.should == 'Autositz'
17 | end
18 |
19 | it "is translated through FastGettext in plural form" do
20 | name = ActiveModel::Name.new(CarSeat)
21 | name.should_receive(:n_).with('Car seat', 'Car seats', 2).and_return('Сиденья')
22 | name.human(count: 2).should == 'Сиденья'
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/gettext_i18n_rails.gemspec:
--------------------------------------------------------------------------------
1 | name = "gettext_i18n_rails"
2 | require "./lib/#{name}/version"
3 |
4 | Gem::Specification.new name, GettextI18nRails::VERSION do |s|
5 | s.summary = "Simple FastGettext Rails integration."
6 | s.authors = ["Michael Grosser"]
7 | s.email = "michael@grosser.it"
8 | s.homepage = "http://github.com/grosser/#{name}"
9 | s.files = `git ls-files lib MIT-LICENSE.txt`.split("\n")
10 | s.license = "MIT"
11 | s.required_ruby_version = '>= 3.2.0'
12 | s.add_runtime_dependency "fast_gettext", ">= 0.9.0"
13 |
14 | s.add_development_dependency "bump"
15 | s.add_development_dependency "gettext"
16 | s.add_development_dependency "haml"
17 | s.add_development_dependency "hamlit"
18 | s.add_development_dependency "rake"
19 | s.add_development_dependency "rails"
20 | s.add_development_dependency "rspec"
21 | s.add_development_dependency "slim"
22 | s.add_development_dependency "sqlite3"
23 | end
24 |
--------------------------------------------------------------------------------
/spec/gettext_i18n_rails/string_interpolate_fix_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "gettext_i18n_rails/string_interpolate_fix"
3 |
4 | describe "String#%" do
5 | it "is not safe if it was not safe" do
6 | result = ("
%{x}" % {:x => 'a'})
7 | result.should == '
a'
8 | result.html_safe?.should == false
9 | end
10 |
11 | xit "stays safe if it was safe" do
12 | result = ("
%{x}".html_safe % {:x => 'a'})
13 | result.should == '
a'
14 | result.html_safe?.should == true
15 | end
16 |
17 | xit "escapes unsafe added to safe" do
18 | result = ("
%{x}".html_safe % {:x => '
'})
19 | result.should == '
<br/>'
20 | result.html_safe?.should == true
21 | end
22 |
23 | it "does not escape unsafe if it was unsafe" do
24 | result = ("
%{x}" % {:x => '
'})
25 | result.should == '
'
26 | result.html_safe?.should == false
27 | end
28 |
29 | it "does not break array replacement" do
30 | "%ssd" % ['a'].should == "asd"
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/base_parser.rb:
--------------------------------------------------------------------------------
1 | require 'gettext_i18n_rails/gettext_hooks'
2 |
3 | module GettextI18nRails
4 | class BaseParser
5 | def self.target?(file)
6 | File.extname(file) == ".#{extension}"
7 | end
8 |
9 | def self.parse(file, options = {}, _msgids = [])
10 | return _msgids unless load_library
11 | code = convert_to_code(File.read(file))
12 | GetText::RubyParser.new(file, options).parse_source(code)
13 | end
14 |
15 | def self.libraries
16 | [extension]
17 | end
18 |
19 | def self.load_library
20 | return true if @library_loaded
21 |
22 | loaded = libraries.detect do |library|
23 | if Gem::Specification.find_all_by_name(library).any?
24 | require library
25 | true
26 | else
27 | false
28 | end
29 | end
30 |
31 | unless loaded
32 | puts "No #{extension} library could be found: #{libraries.join(" or ")}"
33 |
34 | return false
35 | end
36 |
37 | require 'gettext/tools/parser/ruby'
38 | @library_loaded = loaded
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/MIT-LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (C) 2013 Michael Grosser
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails.rb:
--------------------------------------------------------------------------------
1 | require 'gettext_i18n_rails/version'
2 | require 'gettext_i18n_rails/gettext_hooks'
3 |
4 | module GettextI18nRails
5 | IGNORE_TABLES = [/^sitemap_/, /_versions$/, 'schema_migrations', 'sessions', 'delayed_jobs']
6 | end
7 |
8 | # translate from everywhere
9 | require 'fast_gettext'
10 | Object.send(:include, FastGettext::Translation)
11 |
12 | # make translations html_safe if possible and wanted
13 | if "".respond_to?(:html_safe?)
14 | require 'gettext_i18n_rails/html_safe_translations'
15 | Object.send(:include, GettextI18nRails::HtmlSafeTranslations)
16 | end
17 |
18 | # set up the backend
19 | require 'gettext_i18n_rails/backend'
20 | I18n.backend = GettextI18nRails::Backend.new
21 |
22 | # make I18n play nice with FastGettext
23 | require 'gettext_i18n_rails/i18n_hacks'
24 |
25 | # translate activerecord errors
26 | if defined? Rails::Railtie # Rails 3+
27 | # load active_model extensions at the correct point in time
28 | require 'gettext_i18n_rails/railtie'
29 | else
30 | if defined? ActiveRecord
31 | require 'gettext_i18n_rails/active_record'
32 | elsif defined?(ActiveModel)
33 | require 'gettext_i18n_rails/active_model'
34 | end
35 | end
36 |
37 | # make bundle console work in a rails project
38 | require 'gettext_i18n_rails/action_controller' if defined?(ActionController)
39 |
--------------------------------------------------------------------------------
/spec/gettext_i18n_rails/model_attributes_finder_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | require "spec_helper"
3 | require "gettext_i18n_rails/model_attributes_finder"
4 |
5 | module Test
6 | class Application < Rails::Application
7 | end
8 | end
9 |
10 | describe GettextI18nRails::ModelAttributesFinder do
11 | let(:finder) { GettextI18nRails::ModelAttributesFinder.new }
12 |
13 | before do
14 | Rails.application rescue nil
15 | end
16 |
17 | # Rails < 3.0 doesn't have DescendantsTracker.
18 | # Instead of iterating over ObjectSpace (slow) the decision was made NOT to support
19 | # class hierarchies with abstract base classes in Rails 2.x
20 | describe "#find" do
21 | it "returns all AR models" do
22 | keys = finder.find({}).keys
23 | expected = [CarSeat, Part, StiParent, AbstractParentClass, NotConventional]
24 | keys.should =~ expected
25 | end
26 |
27 | it "returns all columns for each model" do
28 | attributes = finder.find({})
29 | attributes[CarSeat].should == ['id', 'seat_color']
30 | attributes[NotConventional].should == ['id', 'name']
31 | attributes[Part].should == ['car_seat_id', 'id', 'name']
32 | attributes[StiParent].should == ['child_attribute', 'id', 'type']
33 | attributes[AbstractParentClass].should == ['another_child_attribute', 'child_attribute', 'id']
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/railtie.rb:
--------------------------------------------------------------------------------
1 | module GettextI18nRails
2 | class Railtie < ::Rails::Railtie
3 | config.gettext_i18n_rails = ActiveSupport::OrderedOptions.new
4 | config.gettext_i18n_rails.msgmerge = nil
5 | config.gettext_i18n_rails.msgcat = nil
6 | config.gettext_i18n_rails.xgettext = nil
7 | config.gettext_i18n_rails.use_for_active_record_attributes = true
8 | config.gettext_i18n_rails.auto_reload = Rails.env.development?
9 |
10 | rake_tasks do
11 | if Gem::Specification.find_all_by_name("gettext", ">= 3.0.2").any?
12 | require 'gettext_i18n_rails/tasks'
13 | end
14 | end
15 |
16 | config.after_initialize do |app|
17 | if app.config.gettext_i18n_rails.use_for_active_record_attributes
18 | ActiveSupport.on_load :active_record do
19 | require 'gettext_i18n_rails/active_model'
20 | end
21 | end
22 |
23 | # Auto-reload .po and .mo files when they change
24 | if app.config.gettext_i18n_rails.auto_reload
25 | po_files = Dir[Rails.root.join("locale/**/*.{po,mo}")]
26 |
27 | reloader = ActiveSupport::FileUpdateChecker.new(po_files) do
28 | FastGettext.translation_repositories.each_value(&:reload)
29 | Rails.logger.info "Reloaded gettext translations"
30 | end
31 |
32 | app.executor.to_run do
33 | reloader.execute_if_updated
34 | end
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/spec/gettext_i18n_rails/slim_parser_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "gettext_i18n_rails/slim_parser"
3 |
4 | describe GettextI18nRails::SlimParser do
5 | let(:parser){ GettextI18nRails::SlimParser }
6 |
7 | describe "#target?" do
8 | it "targets .slim" do
9 | parser.target?('foo/bar/xxx.slim').should == true
10 | end
11 |
12 | it "does not target anything else" do
13 | parser.target?('foo/bar/xxx.erb').should == false
14 | end
15 | end
16 |
17 | describe "#parse" do
18 | it "finds messages in slim" do
19 | with_file 'div = _("xxxx")' do |path|
20 | po = parser.parse(path, {}, [])
21 | po.entries.should match_array([
22 | have_attributes({
23 | msgctxt: nil,
24 | msgid: "xxxx",
25 | type: :normal,
26 | references: ["#{path}:1"]
27 | })
28 | ])
29 | end
30 | end
31 |
32 | it "can parse 1.9 syntax" do
33 | with_file 'div = _("xxxx", foo: :bar)' do |path|
34 | po = parser.parse(path, {}, [])
35 | po.entries.should match_array([
36 | have_attributes({
37 | msgctxt: nil,
38 | msgid: "xxxx",
39 | type: :normal,
40 | references: ["#{path}:1"]
41 | })
42 | ])
43 | end
44 | end
45 |
46 | it "does not find messages in text" do
47 | with_file 'div _("xxxx")' do |path|
48 | po = parser.parse(path, {}, [])
49 | po.entries.empty?.should == true
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/active_model/translation.rb:
--------------------------------------------------------------------------------
1 | module ActiveModel
2 | module Translation
3 | # CarDealer.sales_count -> s_('CarDealer|Sales count') -> 'Sales count' if no translation was found
4 | def human_attribute_name(attribute, *args)
5 | s_(gettext_translation_for_attribute_name(attribute))
6 | end
7 |
8 | def gettext_translation_for_attribute_name(attribute)
9 | attribute = attribute.to_s
10 | if attribute.end_with?('_id')
11 | humanize_class_name(attribute)
12 | else
13 | attribute_key = attribute.split('.').map! {|a| a.humanize }.join('|')
14 | root = inheritance_tree_root(self).to_s
15 |
16 | # in case of STI or no inheritance, first attempt retrieving the key for the current class
17 | sti_key = "#{to_s}|#{attribute_key}"
18 | return sti_key if to_s == root || FastGettext.cached_find(sti_key)
19 |
20 | # fallback to lookup for the root class
21 | return "#{root}|#{attribute_key}"
22 | end
23 | end
24 |
25 | def inheritance_tree_root(aclass)
26 | return aclass unless aclass.respond_to?(:base_class)
27 | base = aclass.base_class
28 | if base.superclass.abstract_class?
29 | if defined?(::ApplicationRecord) && base.superclass == ApplicationRecord
30 | base
31 | else
32 | base.superclass
33 | end
34 | else
35 | base
36 | end
37 | end
38 |
39 | def humanize_class_name(name=nil)
40 | name ||= self.to_s
41 | name.underscore.humanize
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/gettext_i18n_rails/action_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | FastGettext.silence_errors
4 |
5 | describe ActionController::Base do
6 | def reset!
7 | fake_session = {}
8 | @c.stub(:session).and_return fake_session
9 | fake_cookies = {}
10 | @c.stub(:cookies).and_return fake_cookies
11 | @c.params = {}
12 | @c.request = double(:env => {})
13 | end
14 |
15 | before do
16 | #controller
17 | @c = ActionController::Base.new
18 | reset!
19 |
20 | #locale
21 | FastGettext.available_locales = nil
22 | FastGettext.locale = I18n.default_locale = 'fr'
23 | FastGettext.available_locales = ['fr','en']
24 | end
25 |
26 | it "changes the locale" do
27 | @c.params = {:locale=>'en'}
28 | @c.set_gettext_locale
29 | @c.session[:locale].should == 'en'
30 | FastGettext.locale.should == 'en'
31 | end
32 |
33 | it "stays with default locale when none was found" do
34 | @c.set_gettext_locale
35 | @c.session[:locale].should == 'fr'
36 | FastGettext.locale.should == 'fr'
37 | end
38 |
39 | it "locale isn't cached over request" do
40 | @c.params = {:locale=>'en'}
41 | @c.set_gettext_locale
42 | @c.session[:locale].should == 'en'
43 |
44 | reset!
45 | @c.set_gettext_locale
46 | @c.session[:locale].should == 'fr'
47 | end
48 |
49 | it "reads the locale from the HTTP_ACCEPT_LANGUAGE" do
50 | @c.request.stub(:env).and_return 'HTTP_ACCEPT_LANGUAGE'=>'de-de,de;q=0.8,en-us;q=0.5,en;q=0.3'
51 | @c.set_gettext_locale
52 | FastGettext.locale.should == 'en'
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/backend.rb:
--------------------------------------------------------------------------------
1 | module GettextI18nRails
2 | #translates i18n calls to gettext calls
3 | class Backend
4 | @@translate_defaults = true
5 | cattr_accessor :translate_defaults
6 | attr_accessor :backend
7 |
8 | def initialize(*args)
9 | self.backend = I18n::Backend::Simple.new(*args)
10 | end
11 |
12 | def available_locales
13 | FastGettext.available_locales || []
14 | end
15 |
16 | def translate(locale, key, options)
17 | I18n.with_locale(locale) do
18 | if gettext_key = gettext_key(key, options)
19 | translation =
20 | plural_translate(gettext_key, options) || FastGettext._(gettext_key)
21 | interpolate(translation, options)
22 | else
23 | result = backend.translate(locale, key, options)
24 | if result.is_a?(String)
25 | result = result.dup if result.frozen?
26 | result.force_encoding("UTF-8")
27 | else
28 | result
29 | end
30 | end
31 | end
32 | end
33 |
34 | def method_missing(method, *args)
35 | backend.send(method, *args)
36 | end
37 |
38 | protected
39 |
40 | def gettext_key(key, options)
41 | flat_key = flatten_key key, options
42 | if FastGettext.key_exist?(flat_key)
43 | flat_key
44 | elsif self.class.translate_defaults
45 | [*options[:default]].each do |default|
46 | #try the scoped(more specific) key first e.g. 'activerecord.errors.my custom message'
47 | flat_key = flatten_key default, options
48 | return flat_key if FastGettext.key_exist?(flat_key)
49 |
50 | #try the short key thereafter e.g. 'my custom message'
51 | return default if FastGettext.key_exist?(default)
52 | end
53 | return nil
54 | end
55 | end
56 |
57 | def plural_translate(gettext_key, options)
58 | if options[:count]
59 | translation = FastGettext.n_(gettext_key, options[:count])
60 | discard_pass_through_key gettext_key, translation
61 | end
62 | end
63 |
64 | def discard_pass_through_key(key, translation)
65 | if translation == key
66 | nil
67 | else
68 | translation
69 | end
70 | end
71 |
72 | def interpolate(string, values)
73 | if string.respond_to?(:%)
74 | reserved_keys = if defined?(I18n::RESERVED_KEYS) # rails 3+
75 | I18n::RESERVED_KEYS
76 | else
77 | I18n::Backend::Base::RESERVED_KEYS
78 | end
79 |
80 | options = values.except(*reserved_keys)
81 | options.any? ? (string % options) : string
82 | else
83 | string
84 | end
85 | end
86 |
87 | def flatten_key key, options
88 | scope = [*(options[:scope] || [])]
89 | scope.empty? ? key.to_s : "#{scope*'.'}.#{key}"
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/spec/gettext_i18n_rails/active_record_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | require "spec_helper"
3 |
4 | describe ActiveRecord::Base do
5 | before do
6 | FastGettext.reload!
7 | end
8 |
9 | describe :human_name do
10 | it "is translated through FastGettext" do
11 | CarSeat.should_receive(:_).with('Car seat').and_return('Autositz')
12 | CarSeat.human_name.should == 'Autositz'
13 | end
14 | end
15 |
16 | describe :human_attribute_name do
17 | it "translates attributes through FastGettext" do
18 | CarSeat.should_receive(:s_).with('CarSeat|Seat color').and_return('Sitz farbe')
19 | CarSeat.human_attribute_name(:seat_color).should == 'Sitz farbe'
20 | end
21 |
22 | it "translates nested attributes through FastGettext" do
23 | CarSeat.should_receive(:s_).with('CarSeat|Parts|Name').and_return('Handle')
24 | CarSeat.human_attribute_name(:"parts.name").should == 'Handle'
25 | end
26 |
27 | it "translates attributes of STI classes through FastGettext" do
28 | StiChild.should_receive(:s_).with('StiParent|Child attribute').and_return('Kinderattribut')
29 | StiChild.human_attribute_name(:child_attribute).should == 'Kinderattribut'
30 | end
31 |
32 | it "translates attributes of concrete children of abstract parent classes" do
33 | ConcreteChildClass.should_receive(:s_).with('AbstractParentClass|Child attribute').and_return('Kinderattribut')
34 | ConcreteChildClass.human_attribute_name(:child_attribute).should == 'Kinderattribut'
35 | end
36 | end
37 |
38 | describe :gettext_translation_for_attribute_name do
39 | it "translates foreign keys to model name keys" do
40 | Part.gettext_translation_for_attribute_name(:car_seat_id).should == 'Car seat'
41 | end
42 | end
43 |
44 | describe 'error messages' do
45 | let(:model){
46 | c = CarSeat.new
47 | c.valid?
48 | c
49 | }
50 |
51 | it "translates error messages" do
52 | FastGettext.stub(:current_repository).and_return('translate me'=>"Übersetz mich!")
53 | FastGettext._('translate me').should == "Übersetz mich!"
54 | model.errors.full_messages.should == ["Seat color Übersetz mich!"]
55 | end
56 |
57 | it "translates scoped error messages" do
58 | pending 'scope is no longer added in 3.x' if ActiveRecord::VERSION::MAJOR >= 3
59 | FastGettext.stub(:current_repository).and_return('activerecord.errors.translate me'=>"Übersetz mich!")
60 | FastGettext._('activerecord.errors.translate me').should == "Übersetz mich!"
61 | model.errors.full_messages.should == ["Seat color Übersetz mich!"]
62 | end
63 |
64 | it "translates error messages with %{fn}" do
65 | pending
66 | FastGettext.stub(:current_repository).and_return('translate me'=>"Übersetz %{fn} mich!")
67 | FastGettext._('translate me').should == "Übersetz %{fn} mich!"
68 | model.errors[:seat_color].should == ["Übersetz car_seat mich!"]
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/spec/gettext_i18n_rails/haml_parser_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "gettext_i18n_rails/haml_parser"
3 |
4 | describe GettextI18nRails::HamlParser do
5 | let(:parser){ GettextI18nRails::HamlParser }
6 |
7 | describe "#target?" do
8 | it "targets .haml" do
9 | parser.target?('foo/bar/xxx.haml').should == true
10 | end
11 |
12 | it "does not target anything else" do
13 | parser.target?('foo/bar/xxx.erb').should == false
14 | end
15 | end
16 |
17 | describe "#parse" do
18 | ["haml", "hamlit"].each do |library|
19 | context "with #{library} library only" do
20 | before do
21 | GettextI18nRails::HamlParser.stub(:libraries).and_return([library])
22 | GettextI18nRails::HamlParser.instance_variable_set(:@library_loaded, false)
23 | end
24 |
25 | it "finds messages in haml" do
26 | with_file '= _("xxxx")', '.haml' do |path|
27 | po = parser.parse(path, {}, [])
28 | po.entries.should match_array([
29 | have_attributes({
30 | msgctxt: nil,
31 | msgid: "xxxx",
32 | type: :normal,
33 | references: ["#{path}:1"]
34 | })
35 | ])
36 | end
37 | end
38 |
39 | it "finds messages with concatenation" do
40 | with_file '= _("xxxx" + "yyyy" + "zzzz")', '.haml' do |path|
41 | po = parser.parse(path, {}, [])
42 | po.entries.should match_array([
43 | have_attributes({
44 | msgctxt: nil,
45 | msgid: "xxxxyyyyzzzz",
46 | type: :normal,
47 | references: ["#{path}:1"]
48 | })
49 | ])
50 | end
51 | end
52 |
53 | it "finds messages with context in haml" do
54 | with_file '= p_("My context", "idkey")', '.haml' do |path|
55 | po = parser.parse(path, {}, [])
56 | po.entries.should match_array([
57 | have_attributes({
58 | msgctxt: "My context",
59 | msgid: "idkey",
60 | type: :msgctxt,
61 | references: ["#{path}:1"]
62 | })
63 | ])
64 | end
65 | end
66 |
67 | it "should parse the 1.9 if ruby_version is 1.9" do
68 | if RUBY_VERSION =~ /^1\.9/ || RUBY_VERSION > "2"
69 | with_file '= _("xxxx", x: 1)', '.haml' do |path|
70 | po = parser.parse(path, {}, [])
71 | po.entries.should match_array([
72 | have_attributes({
73 | msgctxt: nil,
74 | msgid: "xxxx",
75 | type: :normal,
76 | references: ["#{path}:1"]
77 | })
78 | ])
79 | end
80 | end
81 | end
82 |
83 | it "does not find messages in text" do
84 | with_file '_("xxxx")', '.haml' do |path|
85 | po = parser.parse(path, {}, [])
86 | po.entries.empty?.should == true
87 | end
88 | end
89 |
90 | it "does not include parser options into parsed output" do
91 | with_file '= _("xxxx")' do |path|
92 | GetText::RubyParser.stub(:new).and_return(double("mockparser", parse_source: []))
93 | parser.parse(path, { comment_tag: "TRANSLATORS:" })
94 |
95 | GetText::RubyParser.should have_received(:new).with(path, { comment_tag: "TRANSLATORS:" })
96 | end
97 | end
98 | end
99 | end
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/version'
2 | if RUBY_VERSION > "2" && ActiveSupport::VERSION::MAJOR == 2
3 | warn "Not running ruby 2 vs rails 2 tests"
4 | exit 0
5 | end
6 |
7 | require 'tempfile'
8 | require 'active_support'
9 | require 'active_support/core_ext/string/output_safety'
10 | require 'rails/railtie'
11 | require 'active_record'
12 | require 'action_controller'
13 | require 'action_mailer'
14 | require 'fast_gettext'
15 |
16 | # Define minimal Rails stub for library compatibility
17 | module Rails
18 | def self.root
19 | File.dirname(__FILE__)
20 | end
21 |
22 | def self.env
23 | ActiveSupport::StringInquirer.new('test')
24 | end
25 | end
26 |
27 | require 'gettext_i18n_rails'
28 |
29 | # Manually load ActiveRecord/ActiveModel extensions since we're not running full Rails initialization
30 | # In a real Rails app, these would be loaded via the Railtie's after_initialize hook
31 | require 'gettext_i18n_rails/active_model'
32 | require 'gettext_i18n_rails/active_record'
33 |
34 | require 'temple'
35 |
36 | if ActiveSupport::VERSION::MAJOR >= 3
37 | I18n.enforce_available_locales = false # maybe true ... not sure
38 | end
39 |
40 | RSpec.configure do |config|
41 | config.expect_with(:rspec) { |c| c.syntax = :should }
42 | config.mock_with(:rspec) { |c| c.syntax = :should }
43 | end
44 |
45 | begin
46 | Gem.all_load_paths
47 | rescue
48 | module Gem;def self.all_load_paths;[];end;end
49 | end
50 |
51 |
52 | # make temple not blow up in rails 2 env
53 | class << Temple::Templates
54 | alias_method :method_missing_old, :method_missing
55 | def method_missing(name, engine, options = {})
56 | name == :Rails || method_missing_old(name, engine, options)
57 | end
58 | end
59 |
60 | def with_file(content, extension = '')
61 | Tempfile.open(['gettext_i18n_rails_specs', extension]) do |f|
62 | f.write(content)
63 | f.close
64 | yield f.path
65 | end
66 | end
67 |
68 | ActiveRecord::Base.establish_connection(
69 | :adapter => "sqlite3",
70 | :database => ":memory:"
71 | )
72 |
73 | ActiveRecord::Schema.verbose = false
74 | ActiveRecord::Schema.define(:version => 1) do
75 | create_table :car_seats, :force=>true do |t|
76 | t.string :seat_color
77 | end
78 |
79 | create_table :parts, :force=>true do |t|
80 | t.string :name
81 | t.references :car_seat
82 | end
83 |
84 | create_table :not_at_all_conventionals, :force=>true do |t|
85 | t.string :name
86 | end
87 |
88 | create_table :sti_parents, :force => true do |t|
89 | t.string :type
90 | t.string :child_attribute
91 | end
92 |
93 | create_table :concrete_child_classes, :force => true do |t|
94 | t.string :child_attribute
95 | end
96 |
97 | create_table :other_concrete_child_classes, :force => true do |t|
98 | t.string :another_child_attribute
99 | end
100 | end
101 |
102 | class CarSeat < ActiveRecord::Base
103 | validates_presence_of :seat_color, :message=>"translate me"
104 | has_many :parts
105 | accepts_nested_attributes_for :parts
106 | end
107 |
108 | class Part < ActiveRecord::Base
109 | belongs_to :car_seat
110 | end
111 |
112 | class StiParent < ActiveRecord::Base; end
113 | class StiChild < StiParent; end
114 |
115 | class AbstractParentClass < ActiveRecord::Base
116 | self.abstract_class = true
117 | end
118 | class ConcreteChildClass < AbstractParentClass; end
119 | class OtherConcreteChildClass < AbstractParentClass; end
120 |
121 | class NotConventional < ActiveRecord::Base
122 | if ActiveRecord::VERSION::MAJOR == 2
123 | set_table_name :not_at_all_conventionals
124 | else
125 | self.table_name = :not_at_all_conventionals
126 | end
127 | end
128 |
129 | class Idea < ActiveRecord::Base
130 | self.abstract_class = true
131 | end
132 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/tasks.rb:
--------------------------------------------------------------------------------
1 | require "gettext/tools/task"
2 | gem "gettext", ">= 3.0.2"
3 |
4 | namespace :gettext do
5 | def locale_path
6 | path = FastGettext.translation_repositories[text_domain].instance_variable_get(:@options)[:path] rescue nil
7 | path || File.join(Rails.root, "locale")
8 | end
9 |
10 | def text_domain
11 | # if your textdomain is not 'app': require the environment before calling e.g. gettext:find OR add TEXTDOMAIN=my_domain
12 | (FastGettext.text_domain rescue nil) || ENV['TEXTDOMAIN'] || "app"
13 | end
14 |
15 | # do not rename, gettext_i18n_rails_js overwrites this to inject coffee + js
16 | def files_to_translate
17 | Dir.glob("{app,lib,config,#{locale_path}}/**/*.{rb,erb,haml,slim}")
18 | end
19 |
20 | def gettext_default_options
21 | config = (Rails.application.config.gettext_i18n_rails.default_options if defined?(Rails.application))
22 | config || %w[--sort-by-msgid --no-location --no-wrap]
23 | end
24 |
25 | def gettext_msgmerge_options
26 | config = (Rails.application.config.gettext_i18n_rails.msgmerge if defined?(Rails.application))
27 | config || gettext_default_options
28 | end
29 |
30 | def gettext_msgcat_options
31 | config = (Rails.application.config.gettext_i18n_rails.msgcat if defined?(Rails.application))
32 | config || gettext_default_options - %w[--location]
33 | end
34 |
35 | def gettext_xgettext_options
36 | config = (Rails.application.config.gettext_i18n_rails.xgettext if defined?(Rails.application))
37 | config || gettext_default_options
38 | end
39 |
40 | require "gettext_i18n_rails/haml_parser"
41 | require "gettext_i18n_rails/slim_parser"
42 |
43 | task :setup => [:environment] do
44 | GetText::Tools::Task.define do |task|
45 | task.package_name = text_domain
46 | task.package_version = "1.0.0"
47 | task.domain = text_domain
48 | task.po_base_directory = locale_path
49 | task.mo_base_directory = locale_path
50 | task.files = files_to_translate
51 | task.enable_description = false
52 | task.msgmerge_options = gettext_msgmerge_options
53 | task.msgcat_options = gettext_msgcat_options
54 | task.xgettext_options = gettext_xgettext_options
55 | end
56 | end
57 |
58 | desc "Create mo-files"
59 | task :pack => [:setup] do
60 | Rake::Task["gettext:mo:update"].invoke
61 | end
62 |
63 | desc "Update pot/po files"
64 | task :find => [:setup] do
65 | Rake::Task["gettext:po:update"].invoke
66 | end
67 |
68 | # This is more of an example, ignoring
69 | # the columns/tables that mostly do not need translation.
70 | # This can also be done with GetText::ActiveRecord
71 | # but this crashed too often for me, and
72 | # IMO which column should/should-not be translated does not
73 | # belong into the model
74 | #
75 | # You can get your translations from GetText::ActiveRecord
76 | # by adding this to you gettext:find task
77 | #
78 | # require 'active_record'
79 | # gem "gettext_activerecord", '>=0.1.0' #download and install from github
80 | # require 'gettext_activerecord/parser'
81 | desc "write the model attributes to /model_attributes.rb"
82 | task :store_model_attributes => :environment do
83 | FastGettext.silence_errors
84 |
85 | require 'gettext_i18n_rails/model_attributes_finder'
86 | require 'gettext_i18n_rails/active_record'
87 |
88 | storage_file = "#{locale_path}/model_attributes.rb"
89 | puts "writing model translations to: #{storage_file}"
90 |
91 | GettextI18nRails.store_model_attributes(
92 | :to => storage_file,
93 | :ignore_columns => [/_id$/, 'id', 'type', 'created_at', 'updated_at'],
94 | :ignore_tables => GettextI18nRails::IGNORE_TABLES
95 | )
96 | end
97 |
98 | desc "add a new language"
99 | task :add_language, [:language] => :environment do |_, args|
100 | language = args.language || ENV["LANGUAGE"]
101 |
102 | # Let's do some pre-verification of the environment.
103 | if language.nil?
104 | puts "You need to specify the language to add. Either 'LANGUAGE=eo rake gettext:add_language' or 'rake gettext:add_language[eo]'"
105 | next
106 | end
107 |
108 | language_path = File.join(locale_path, language)
109 | mkdir_p(language_path)
110 | Rake.application.lookup('gettext:find', _.scope).invoke
111 | end
112 | end
113 |
--------------------------------------------------------------------------------
/spec/gettext_i18n_rails_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | FastGettext.silence_errors
4 |
5 | describe GettextI18nRails do
6 | before do
7 | GettextI18nRails.translations_are_html_safe = nil
8 | end
9 |
10 | it "extends all classes with fast_gettext" do
11 | _('test')
12 | end
13 |
14 | describe 'translations_are_html_safe' do
15 | before do
16 | GettextI18nRails.translations_are_html_safe = nil
17 | end
18 |
19 | it "makes translations not html_safe by default" do
20 | _('x').html_safe?.should == false
21 | s_('x').html_safe?.should == false
22 | n_('x','y',2).html_safe?.should == false
23 | String._('x').html_safe?.should == false
24 | String.s_('x').html_safe?.should == false
25 | String.n_('x','y',2).html_safe?.should == false
26 | end
27 |
28 | it "makes instance translations html_safe when wanted" do
29 | GettextI18nRails.translations_are_html_safe = true
30 | _('x').html_safe?.should == true
31 | s_('x').html_safe?.should == true
32 | n_('x','y',2).html_safe?.should == true
33 | end
34 |
35 | it "makes class translations html_safe when wanted" do
36 | GettextI18nRails.translations_are_html_safe = true
37 | String._('x').html_safe?.should == true
38 | String.s_('x').html_safe?.should == true
39 | String.n_('x','y',2).html_safe?.should == true
40 | end
41 |
42 | it "does not make everything html_safe" do
43 | 'x'.html_safe?.should == false
44 | end
45 | end
46 |
47 | it "sets up out backend" do
48 | I18n.backend.is_a?(GettextI18nRails::Backend).should == true
49 | end
50 |
51 | it "has a VERSION" do
52 | GettextI18nRails::VERSION.should =~ /^\d+\.\d+\.\d+$/
53 | end
54 |
55 | describe 'FastGettext I18n interaction' do
56 | before do
57 | FastGettext.available_locales = nil
58 | FastGettext.locale = 'de'
59 | end
60 |
61 | it "links FastGettext with I18n locale" do
62 | FastGettext.locale = 'xx'
63 | I18n.locale.should == :xx
64 | end
65 |
66 | it "does not set an not-accepted locale to I18n.locale" do
67 | FastGettext.available_locales = ['de']
68 | FastGettext.locale = 'xx'
69 | I18n.locale.should == :de
70 | end
71 |
72 | it "links I18n.locale and FastGettext.locale" do
73 | I18n.locale = :yy
74 | FastGettext.locale.should == 'yy'
75 | end
76 |
77 | it "does not set a non-available locale though I18n.locale" do
78 | FastGettext.available_locales = ['de']
79 | I18n.locale = :xx
80 | FastGettext.locale.should == 'de'
81 | I18n.locale.should == :de
82 | end
83 |
84 | it "converts gettext to i18n style for nested locales" do
85 | FastGettext.available_locales = ['de_CH']
86 | I18n.locale = :"de-CH"
87 | FastGettext.locale.should == 'de_CH'
88 | I18n.locale.should == :"de-CH"
89 | end
90 | end
91 |
92 | describe "GetText PO file creation" do
93 | before do
94 | require "gettext_i18n_rails/haml_parser"
95 | require "gettext_i18n_rails/slim_parser"
96 | end
97 |
98 | it "parses haml" do
99 | haml_content = <<~EOR
100 | = _("xxxx")
101 | = p_("Context", "key")
102 | _("JustText")
103 | EOR
104 | with_file haml_content, '.haml' do |path|
105 | po = GettextI18nRails::GettextHooks.xgettext.new.parse(path)
106 | po.entries.should match_array([
107 | have_attributes({
108 | msgctxt: nil,
109 | msgid: "xxxx",
110 | type: :normal,
111 | references: ["#{path}:1"]
112 | }),
113 | have_attributes({
114 | msgctxt: "Context",
115 | msgid: "key",
116 | type: :msgctxt,
117 | references: ["#{path}:2"]
118 | })
119 | ])
120 | end
121 | end
122 |
123 | it "parses slim" do
124 | slim_content = <<~EOR
125 | div = _("xxxx")
126 | div = p_("Context", "key")
127 | div _("JustText")
128 | EOR
129 | with_file slim_content, '.slim' do |path|
130 | po = GettextI18nRails::GettextHooks.xgettext.new.parse(path)
131 | po.entries.should match_array([
132 | have_attributes({
133 | msgctxt: nil,
134 | msgid: "xxxx",
135 | type: :normal,
136 | references: ["#{path}:1"]
137 | }),
138 | have_attributes({
139 | msgctxt: "Context",
140 | msgid: "key",
141 | type: :msgctxt,
142 | references: ["#{path}:2"]
143 | })
144 | ])
145 | end
146 | end
147 | end
148 | end
149 |
--------------------------------------------------------------------------------
/lib/gettext_i18n_rails/model_attributes_finder.rb:
--------------------------------------------------------------------------------
1 | require 'rails/version'
2 | require 'rails'
3 |
4 | module GettextI18nRails
5 | #write all found models/columns to a file where GetTexts ruby parser can find them
6 | def store_model_attributes(options)
7 | file = options[:to] || 'locale/model_attributes.rb'
8 | begin
9 | File.open(file,'w') do |f|
10 | f.puts "#DO NOT MODIFY! AUTOMATICALLY GENERATED FILE!"
11 | ModelAttributesFinder.new.find(options).each do |model,column_names|
12 | f.puts("_('#{model.humanize_class_name}')")
13 |
14 | #all columns namespaced under the model
15 | column_names.each do |attribute|
16 | translation = model.gettext_translation_for_attribute_name(attribute)
17 | f.puts("_('#{translation}')")
18 | end
19 | end
20 | f.puts "#DO NOT MODIFY! AUTOMATICALLY GENERATED FILE!"
21 | f.puts "{}"
22 | end
23 | rescue
24 | puts "[Error] Attribute extraction failed. Removing incomplete file (#{file})"
25 | File.delete(file)
26 | raise
27 | end
28 | end
29 | module_function :store_model_attributes
30 |
31 | class ModelAttributesFinder
32 | # options:
33 | # :ignore_tables => ['cars',/_settings$/,...]
34 | # :ignore_columns => ['id',/_id$/,...]
35 | # current connection ---> {'cars'=>['model_name','type'],...}
36 | def find(options)
37 | found = ActiveSupport::OrderedHash.new([])
38 | models.each do |model|
39 | attributes = model_attributes(model, options[:ignore_tables], options[:ignore_columns])
40 | found[model] = attributes.sort if attributes.any?
41 | end
42 | found
43 | end
44 |
45 | def initialize
46 | @existing_tables = ::ActiveRecord::Base.connection.data_sources
47 | end
48 |
49 | # Rails < 3.0 doesn't have DescendantsTracker.
50 | # Instead of iterating over ObjectSpace (slow) the decision was made NOT to support
51 | # class hierarchies with abstract base classes in Rails 2.x
52 | def model_attributes(model, ignored_tables, ignored_cols)
53 | if model.abstract_class?
54 | model.subclasses.reject {|m| ignored?(m.table_name, ignored_tables)}.inject([]) do |attrs, m|
55 | attrs.push(model_attributes(m, ignored_tables, ignored_cols)).flatten.uniq
56 | end
57 | elsif !ignored?(model.table_name, ignored_tables) && @existing_tables.include?(model.table_name)
58 | model.columns.reject { |c| ignored?(c.name, ignored_cols) }.collect { |c| c.name }
59 | else
60 | []
61 | end
62 | end
63 |
64 | def models
65 | # Ensure autoloaders are set up before we attempt to eager load!
66 | Rails.application.autoloaders.each(&:setup) if Rails.application.respond_to?(:autoloaders)
67 | Rails.application.eager_load! # make sure that all models are loaded so that direct_descendants works
68 | descendants = ::ActiveRecord::Base.subclasses
69 |
70 | # In rails 5+ user models are supposed to inherit from ApplicationRecord
71 | if defined?(::ApplicationRecord)
72 | descendants += ApplicationRecord.subclasses
73 | descendants.delete ApplicationRecord
74 | end
75 |
76 | descendants.uniq.sort_by(&:name)
77 | end
78 |
79 | def ignored?(name,patterns)
80 | return false unless patterns
81 | patterns.detect{|p|p.to_s==name.to_s or (p.is_a?(Regexp) and name=~p)}
82 | end
83 |
84 | private
85 | # Tries to find the model class corresponding to specified table name.
86 | # Takes into account that the model can be defined in a namespace.
87 | # Searches only up to one level deep - won't find models nested in two
88 | # or more modules.
89 | #
90 | # Note that if we allow namespaces, the conversion can be ambiguous, i.e.
91 | # if the table is named "aa_bb_cc" and AaBbCc, Aa::BbCc and AaBb::Cc are
92 | # all defined there's no absolute rule that tells us which one to use.
93 | # This method prefers the less nested one and, if there are two at
94 | # the same level, the one with shorter module name.
95 | def table_name_to_namespaced_model(table_name)
96 | # First assume that there are no namespaces
97 | model = to_class(table_name.singularize.camelcase)
98 | return model if model != nil
99 |
100 | # If you were wrong, assume that the model is in a namespace.
101 | # Iterate over the underscores and try to substitute each of them
102 | # for a slash that camelcase() replaces with the scope operator (::).
103 | underscore_position = table_name.index('_')
104 | while underscore_position != nil
105 | namespaced_table_name = table_name.dup
106 | namespaced_table_name[underscore_position] = '/'
107 | model = to_class(namespaced_table_name.singularize.camelcase)
108 | return model if model != nil
109 |
110 | underscore_position = table_name.index('_', underscore_position + 1)
111 | end
112 |
113 | # The model either is not defined or is buried more than one level
114 | # deep in a module hierarchy
115 | return nil
116 | end
117 |
118 | # Checks if there is a class of specified name and if so, returns
119 | # the class object. Otherwise returns nil.
120 | def to_class(name)
121 | # I wanted to use Module.const_defined?() here to avoid relying
122 | # on exceptions for normal program flow but it's of no use.
123 | # If class autoloading is enabled, the constant may be undefined
124 | # but turn out to be present when we actually try to use it.
125 | begin
126 | constant = name.constantize
127 | rescue NameError
128 | return nil
129 | rescue LoadError => e
130 | $stderr.puts "failed to load '#{name}', ignoring (#{e.class}: #{e.message})"
131 | return nil
132 | end
133 |
134 | return constant.is_a?(Class) ? constant : nil
135 | end
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/spec/gettext_i18n_rails/backend_spec.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require "spec_helper"
3 |
4 | FastGettext.silence_errors
5 |
6 | describe GettextI18nRails::Backend do
7 | subject { GettextI18nRails::Backend.new }
8 |
9 | before do
10 | FastGettext.reload!
11 | end
12 |
13 | it "redirects calls to another I18n backend" do
14 | subject.backend.should_receive(:xxx).with(1,2)
15 | subject.xxx(1,2)
16 | end
17 |
18 | describe :available_locales do
19 | it "maps them to FastGettext" do
20 | FastGettext.should_receive(:available_locales).and_return [:xxx]
21 | subject.available_locales.should == [:xxx]
22 | end
23 |
24 | it "and returns an empty array when FastGettext.available_locales is nil" do
25 | FastGettext.should_receive(:available_locales).and_return nil
26 | subject.available_locales.should == []
27 | end
28 | end
29 |
30 | describe :translate do
31 | it "uses gettext when the key is translatable" do
32 | FastGettext.should_receive(:current_repository).and_return 'xy.z.u'=>'a'
33 | subject.translate('xx','u',:scope=>['xy','z']).should == 'a'
34 | end
35 |
36 | it "interpolates options" do
37 | FastGettext.should_receive(:current_repository).and_return 'ab.c'=>'a%{a}b'
38 | subject.translate('xx','c',:scope=>['ab'], :a => 'X').should == 'aXb'
39 | end
40 |
41 | it "will not try and interpolate when there are no options given" do
42 | FastGettext.should_receive(:current_repository).and_return 'ab.d' => 'a%{a}b'
43 | subject.translate('xx', 'd', :scope=>['ab']).should == 'a%{a}b'
44 | end
45 |
46 | it "uses plural translation if count is given" do
47 | repo = {'ab.e' => 'existing'}
48 | repo.should_receive(:plural).and_return %w(single plural)
49 | repo.stub(:pluralisation_rule).and_return nil
50 | FastGettext.stub(:current_repository).and_return repo
51 | subject.translate('xx', 'ab.e', :count => 1).should == 'single'
52 | subject.translate('xx', 'ab.e', :count => 2).should == 'plural'
53 | end
54 |
55 | it "interpolates a count without plural translations" do
56 | repo = {'ab.e' => 'existing %{count}'}
57 | repo.should_receive(:plural).and_return []
58 | repo.stub(:pluralisation_rule).and_return lambda { |i| true }
59 | FastGettext.stub(:current_repository).and_return repo
60 | FastGettext.should_receive(:set_locale).with('xx').and_return('xx')
61 | FastGettext.should_receive(:set_locale).with(:en).and_return(:en)
62 | subject.translate('xx', 'ab.e', :count => 1).should == 'existing 1'
63 | end
64 |
65 | it "can translate with gettext using symbols" do
66 | FastGettext.should_receive(:current_repository).and_return 'xy.z.v'=>'a'
67 | subject.translate('xx',:v ,:scope=>['xy','z']).should == 'a'
68 | end
69 |
70 | it "can translate with gettext using a flat scope" do
71 | FastGettext.should_receive(:current_repository).and_return 'xy.z.x'=>'a'
72 | subject.translate('xx',:x ,:scope=>'xy.z').should == 'a'
73 | end
74 |
75 | it "passes non-gettext keys to default backend" do
76 | subject.backend.should_receive(:translate).with('xx', 'c', {}).and_return 'd'
77 | # TODO track down why this is called 3 times on 1.8 (only 1 time on 1.9)
78 | FastGettext.stub(:current_repository).and_return 'a'=>'b'
79 | subject.translate('xx', 'c', {}).should == 'd'
80 | end
81 |
82 | it "passes non-gettext keys to default backend without modifying frozen translation" do
83 | subject.backend.should_receive(:translate).with('xx', 'c', {}).and_return 'd'.freeze
84 | # TODO track down why this is called 3 times on 1.8 (only 1 time on 1.9)
85 | FastGettext.stub(:current_repository).and_return 'a'=>'b'
86 | subject.translate('xx', 'c', {}).should == 'd'
87 | end
88 |
89 | it 'temporarily sets the given locale' do
90 | FastGettext.should_receive(:set_locale).with('xx').and_return('xy')
91 | FastGettext.should_receive(:set_locale).with('xy').and_return('xx')
92 | FastGettext.should_receive(:set_locale).with(:en).and_return('xx')
93 | subject.backend.should_receive(:translate).with('xx', 'c', {}).and_return 'd'
94 | FastGettext.locale= 'xy'
95 | FastGettext.stub(:current_repository).and_return 'a'=>'b'
96 | subject.translate('xx', 'c', {}).should == 'd'
97 | end
98 |
99 | if RUBY_VERSION > "1.9"
100 | it "produces UTF-8 when not using FastGettext to fix weird encoding bug" do
101 | subject.backend.should_receive(:translate).with('xx', 'c', {}).and_return 'ü'.force_encoding("US-ASCII")
102 | FastGettext.should_receive(:set_locale).with('xx').and_return('xx')
103 | FastGettext.should_receive(:set_locale).with(:en).and_return(:en)
104 | FastGettext.should_receive(:current_repository).and_return 'a'=>'b'
105 | result = subject.translate('xx', 'c', {})
106 | result.should == 'ü'
107 | end
108 |
109 | it "does not force_encoding on non-strings" do
110 | subject.backend.should_receive(:translate).with('xx', 'c', {}).and_return ['aa']
111 | FastGettext.should_receive(:set_locale).with('xx').and_return('xx')
112 | FastGettext.should_receive(:set_locale).with(:en).and_return(:en)
113 | FastGettext.should_receive(:current_repository).and_return 'a'=>'b'
114 | result = subject.translate('xx', 'c', {})
115 | result.should == ['aa']
116 | end
117 | end
118 |
119 | # TODO NameError is raised <-> wtf ?
120 | xit "uses the super when the key is not translatable" do
121 | lambda{subject.translate('xx','y',:scope=>['xy','z'])}.should raise_error(I18n::MissingTranslationData)
122 | end
123 | end
124 |
125 | describe :interpolate do
126 | it "act as an identity function for an array" do
127 | translation = [:month, :day, :year]
128 | subject.send(:interpolate, translation, {}).should == translation
129 | end
130 | end
131 | end
132 |
--------------------------------------------------------------------------------
/gemfiles/rails72.gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: ..
3 | specs:
4 | gettext_i18n_rails (2.1.0)
5 | fast_gettext (>= 0.9.0)
6 |
7 | GEM
8 | remote: https://rubygems.org/
9 | specs:
10 | actioncable (7.2.3)
11 | actionpack (= 7.2.3)
12 | activesupport (= 7.2.3)
13 | nio4r (~> 2.0)
14 | websocket-driver (>= 0.6.1)
15 | zeitwerk (~> 2.6)
16 | actionmailbox (7.2.3)
17 | actionpack (= 7.2.3)
18 | activejob (= 7.2.3)
19 | activerecord (= 7.2.3)
20 | activestorage (= 7.2.3)
21 | activesupport (= 7.2.3)
22 | mail (>= 2.8.0)
23 | actionmailer (7.2.3)
24 | actionpack (= 7.2.3)
25 | actionview (= 7.2.3)
26 | activejob (= 7.2.3)
27 | activesupport (= 7.2.3)
28 | mail (>= 2.8.0)
29 | rails-dom-testing (~> 2.2)
30 | actionpack (7.2.3)
31 | actionview (= 7.2.3)
32 | activesupport (= 7.2.3)
33 | cgi
34 | nokogiri (>= 1.8.5)
35 | racc
36 | rack (>= 2.2.4, < 3.3)
37 | rack-session (>= 1.0.1)
38 | rack-test (>= 0.6.3)
39 | rails-dom-testing (~> 2.2)
40 | rails-html-sanitizer (~> 1.6)
41 | useragent (~> 0.16)
42 | actiontext (7.2.3)
43 | actionpack (= 7.2.3)
44 | activerecord (= 7.2.3)
45 | activestorage (= 7.2.3)
46 | activesupport (= 7.2.3)
47 | globalid (>= 0.6.0)
48 | nokogiri (>= 1.8.5)
49 | actionview (7.2.3)
50 | activesupport (= 7.2.3)
51 | builder (~> 3.1)
52 | cgi
53 | erubi (~> 1.11)
54 | rails-dom-testing (~> 2.2)
55 | rails-html-sanitizer (~> 1.6)
56 | activejob (7.2.3)
57 | activesupport (= 7.2.3)
58 | globalid (>= 0.3.6)
59 | activemodel (7.2.3)
60 | activesupport (= 7.2.3)
61 | activerecord (7.2.3)
62 | activemodel (= 7.2.3)
63 | activesupport (= 7.2.3)
64 | timeout (>= 0.4.0)
65 | activestorage (7.2.3)
66 | actionpack (= 7.2.3)
67 | activejob (= 7.2.3)
68 | activerecord (= 7.2.3)
69 | activesupport (= 7.2.3)
70 | marcel (~> 1.0)
71 | activesupport (7.2.3)
72 | base64
73 | benchmark (>= 0.3)
74 | bigdecimal
75 | concurrent-ruby (~> 1.0, >= 1.3.1)
76 | connection_pool (>= 2.2.5)
77 | drb
78 | i18n (>= 1.6, < 2)
79 | logger (>= 1.4.2)
80 | minitest (>= 5.1)
81 | securerandom (>= 0.3)
82 | tzinfo (~> 2.0, >= 2.0.5)
83 | base64 (0.3.0)
84 | benchmark (0.5.0)
85 | bigdecimal (3.3.1)
86 | builder (3.3.0)
87 | bump (0.10.0)
88 | cgi (0.5.0)
89 | concurrent-ruby (1.3.5)
90 | connection_pool (2.5.4)
91 | crass (1.0.6)
92 | date (3.5.0)
93 | diff-lcs (1.6.2)
94 | drb (2.2.3)
95 | erb (5.1.3)
96 | erubi (1.13.1)
97 | fast_gettext (4.1.1)
98 | prime
99 | racc
100 | forwardable (1.3.3)
101 | gettext (3.5.1)
102 | erubi
103 | locale (>= 2.0.5)
104 | prime
105 | racc
106 | text (>= 1.3.0)
107 | globalid (1.3.0)
108 | activesupport (>= 6.1)
109 | haml (7.0.1)
110 | temple (>= 0.8.2)
111 | thor
112 | tilt
113 | hamlit (4.0.0)
114 | temple (>= 0.8.2)
115 | thor
116 | tilt
117 | i18n (1.14.7)
118 | concurrent-ruby (~> 1.0)
119 | io-console (0.8.1)
120 | irb (1.15.3)
121 | pp (>= 0.6.0)
122 | rdoc (>= 4.0.0)
123 | reline (>= 0.4.2)
124 | locale (2.1.4)
125 | logger (1.7.0)
126 | loofah (2.24.1)
127 | crass (~> 1.0.2)
128 | nokogiri (>= 1.12.0)
129 | mail (2.9.0)
130 | logger
131 | mini_mime (>= 0.1.1)
132 | net-imap
133 | net-pop
134 | net-smtp
135 | marcel (1.1.0)
136 | mini_mime (1.1.5)
137 | minitest (5.26.0)
138 | net-imap (0.5.12)
139 | date
140 | net-protocol
141 | net-pop (0.1.2)
142 | net-protocol
143 | net-protocol (0.2.2)
144 | timeout
145 | net-smtp (0.5.1)
146 | net-protocol
147 | nio4r (2.7.5)
148 | nokogiri (1.18.10-arm64-darwin)
149 | racc (~> 1.4)
150 | nokogiri (1.18.10-x86_64-linux-gnu)
151 | racc (~> 1.4)
152 | pp (0.6.3)
153 | prettyprint
154 | prettyprint (0.2.0)
155 | prime (0.1.4)
156 | forwardable
157 | singleton
158 | psych (5.2.6)
159 | date
160 | stringio
161 | racc (1.8.1)
162 | rack (3.2.4)
163 | rack-session (2.1.1)
164 | base64 (>= 0.1.0)
165 | rack (>= 3.0.0)
166 | rack-test (2.2.0)
167 | rack (>= 1.3)
168 | rackup (2.2.1)
169 | rack (>= 3)
170 | rails (7.2.3)
171 | actioncable (= 7.2.3)
172 | actionmailbox (= 7.2.3)
173 | actionmailer (= 7.2.3)
174 | actionpack (= 7.2.3)
175 | actiontext (= 7.2.3)
176 | actionview (= 7.2.3)
177 | activejob (= 7.2.3)
178 | activemodel (= 7.2.3)
179 | activerecord (= 7.2.3)
180 | activestorage (= 7.2.3)
181 | activesupport (= 7.2.3)
182 | bundler (>= 1.15.0)
183 | railties (= 7.2.3)
184 | rails-dom-testing (2.3.0)
185 | activesupport (>= 5.0.0)
186 | minitest
187 | nokogiri (>= 1.6)
188 | rails-html-sanitizer (1.6.2)
189 | loofah (~> 2.21)
190 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
191 | railties (7.2.3)
192 | actionpack (= 7.2.3)
193 | activesupport (= 7.2.3)
194 | cgi
195 | irb (~> 1.13)
196 | rackup (>= 1.0.0)
197 | rake (>= 12.2)
198 | thor (~> 1.0, >= 1.2.2)
199 | tsort (>= 0.2)
200 | zeitwerk (~> 2.6)
201 | rake (13.3.1)
202 | rdoc (6.15.1)
203 | erb
204 | psych (>= 4.0.0)
205 | tsort
206 | reline (0.6.2)
207 | io-console (~> 0.5)
208 | rspec (3.13.2)
209 | rspec-core (~> 3.13.0)
210 | rspec-expectations (~> 3.13.0)
211 | rspec-mocks (~> 3.13.0)
212 | rspec-core (3.13.6)
213 | rspec-support (~> 3.13.0)
214 | rspec-expectations (3.13.5)
215 | diff-lcs (>= 1.2.0, < 2.0)
216 | rspec-support (~> 3.13.0)
217 | rspec-mocks (3.13.7)
218 | diff-lcs (>= 1.2.0, < 2.0)
219 | rspec-support (~> 3.13.0)
220 | rspec-support (3.13.6)
221 | securerandom (0.4.1)
222 | singleton (0.3.0)
223 | slim (5.2.1)
224 | temple (~> 0.10.0)
225 | tilt (>= 2.1.0)
226 | sqlite3 (2.7.4-arm64-darwin)
227 | sqlite3 (2.7.4-x86_64-linux-gnu)
228 | stringio (3.1.7)
229 | temple (0.10.4)
230 | text (1.3.1)
231 | thor (1.4.0)
232 | tilt (2.6.1)
233 | timeout (0.4.4)
234 | tsort (0.2.0)
235 | tzinfo (2.0.6)
236 | concurrent-ruby (~> 1.0)
237 | useragent (0.16.11)
238 | websocket-driver (0.8.0)
239 | base64
240 | websocket-extensions (>= 0.1.0)
241 | websocket-extensions (0.1.5)
242 | zeitwerk (2.7.3)
243 |
244 | PLATFORMS
245 | arm64-darwin-24
246 | x86_64-linux
247 |
248 | DEPENDENCIES
249 | bump
250 | gettext
251 | gettext_i18n_rails!
252 | haml
253 | hamlit
254 | rails (~> 7.2.0)
255 | rake
256 | rspec
257 | slim
258 | sqlite3
259 |
260 | BUNDLED WITH
261 | 2.4.13
262 |
--------------------------------------------------------------------------------
/gemfiles/rails80.gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: ..
3 | specs:
4 | gettext_i18n_rails (2.1.0)
5 | fast_gettext (>= 0.9.0)
6 |
7 | GEM
8 | remote: https://rubygems.org/
9 | specs:
10 | action_text-trix (2.1.15)
11 | railties
12 | actioncable (8.1.1)
13 | actionpack (= 8.1.1)
14 | activesupport (= 8.1.1)
15 | nio4r (~> 2.0)
16 | websocket-driver (>= 0.6.1)
17 | zeitwerk (~> 2.6)
18 | actionmailbox (8.1.1)
19 | actionpack (= 8.1.1)
20 | activejob (= 8.1.1)
21 | activerecord (= 8.1.1)
22 | activestorage (= 8.1.1)
23 | activesupport (= 8.1.1)
24 | mail (>= 2.8.0)
25 | actionmailer (8.1.1)
26 | actionpack (= 8.1.1)
27 | actionview (= 8.1.1)
28 | activejob (= 8.1.1)
29 | activesupport (= 8.1.1)
30 | mail (>= 2.8.0)
31 | rails-dom-testing (~> 2.2)
32 | actionpack (8.1.1)
33 | actionview (= 8.1.1)
34 | activesupport (= 8.1.1)
35 | nokogiri (>= 1.8.5)
36 | rack (>= 2.2.4)
37 | rack-session (>= 1.0.1)
38 | rack-test (>= 0.6.3)
39 | rails-dom-testing (~> 2.2)
40 | rails-html-sanitizer (~> 1.6)
41 | useragent (~> 0.16)
42 | actiontext (8.1.1)
43 | action_text-trix (~> 2.1.15)
44 | actionpack (= 8.1.1)
45 | activerecord (= 8.1.1)
46 | activestorage (= 8.1.1)
47 | activesupport (= 8.1.1)
48 | globalid (>= 0.6.0)
49 | nokogiri (>= 1.8.5)
50 | actionview (8.1.1)
51 | activesupport (= 8.1.1)
52 | builder (~> 3.1)
53 | erubi (~> 1.11)
54 | rails-dom-testing (~> 2.2)
55 | rails-html-sanitizer (~> 1.6)
56 | activejob (8.1.1)
57 | activesupport (= 8.1.1)
58 | globalid (>= 0.3.6)
59 | activemodel (8.1.1)
60 | activesupport (= 8.1.1)
61 | activerecord (8.1.1)
62 | activemodel (= 8.1.1)
63 | activesupport (= 8.1.1)
64 | timeout (>= 0.4.0)
65 | activestorage (8.1.1)
66 | actionpack (= 8.1.1)
67 | activejob (= 8.1.1)
68 | activerecord (= 8.1.1)
69 | activesupport (= 8.1.1)
70 | marcel (~> 1.0)
71 | activesupport (8.1.1)
72 | base64
73 | bigdecimal
74 | concurrent-ruby (~> 1.0, >= 1.3.1)
75 | connection_pool (>= 2.2.5)
76 | drb
77 | i18n (>= 1.6, < 2)
78 | json
79 | logger (>= 1.4.2)
80 | minitest (>= 5.1)
81 | securerandom (>= 0.3)
82 | tzinfo (~> 2.0, >= 2.0.5)
83 | uri (>= 0.13.1)
84 | base64 (0.3.0)
85 | bigdecimal (3.3.1)
86 | builder (3.3.0)
87 | bump (0.10.0)
88 | concurrent-ruby (1.3.5)
89 | connection_pool (2.5.4)
90 | crass (1.0.6)
91 | date (3.5.0)
92 | diff-lcs (1.6.2)
93 | drb (2.2.3)
94 | erb (5.1.3)
95 | erubi (1.13.1)
96 | fast_gettext (4.1.1)
97 | prime
98 | racc
99 | forwardable (1.3.3)
100 | gettext (3.5.1)
101 | erubi
102 | locale (>= 2.0.5)
103 | prime
104 | racc
105 | text (>= 1.3.0)
106 | globalid (1.3.0)
107 | activesupport (>= 6.1)
108 | haml (7.0.1)
109 | temple (>= 0.8.2)
110 | thor
111 | tilt
112 | hamlit (4.0.0)
113 | temple (>= 0.8.2)
114 | thor
115 | tilt
116 | i18n (1.14.7)
117 | concurrent-ruby (~> 1.0)
118 | io-console (0.8.1)
119 | irb (1.15.3)
120 | pp (>= 0.6.0)
121 | rdoc (>= 4.0.0)
122 | reline (>= 0.4.2)
123 | json (2.15.2)
124 | locale (2.1.4)
125 | logger (1.7.0)
126 | loofah (2.24.1)
127 | crass (~> 1.0.2)
128 | nokogiri (>= 1.12.0)
129 | mail (2.9.0)
130 | logger
131 | mini_mime (>= 0.1.1)
132 | net-imap
133 | net-pop
134 | net-smtp
135 | marcel (1.1.0)
136 | mini_mime (1.1.5)
137 | minitest (5.26.0)
138 | net-imap (0.5.12)
139 | date
140 | net-protocol
141 | net-pop (0.1.2)
142 | net-protocol
143 | net-protocol (0.2.2)
144 | timeout
145 | net-smtp (0.5.1)
146 | net-protocol
147 | nio4r (2.7.5)
148 | nokogiri (1.18.10-arm64-darwin)
149 | racc (~> 1.4)
150 | nokogiri (1.18.10-x86_64-linux-gnu)
151 | racc (~> 1.4)
152 | pp (0.6.3)
153 | prettyprint
154 | prettyprint (0.2.0)
155 | prime (0.1.4)
156 | forwardable
157 | singleton
158 | psych (5.2.6)
159 | date
160 | stringio
161 | racc (1.8.1)
162 | rack (3.2.4)
163 | rack-session (2.1.1)
164 | base64 (>= 0.1.0)
165 | rack (>= 3.0.0)
166 | rack-test (2.2.0)
167 | rack (>= 1.3)
168 | rackup (2.2.1)
169 | rack (>= 3)
170 | rails (8.1.1)
171 | actioncable (= 8.1.1)
172 | actionmailbox (= 8.1.1)
173 | actionmailer (= 8.1.1)
174 | actionpack (= 8.1.1)
175 | actiontext (= 8.1.1)
176 | actionview (= 8.1.1)
177 | activejob (= 8.1.1)
178 | activemodel (= 8.1.1)
179 | activerecord (= 8.1.1)
180 | activestorage (= 8.1.1)
181 | activesupport (= 8.1.1)
182 | bundler (>= 1.15.0)
183 | railties (= 8.1.1)
184 | rails-dom-testing (2.3.0)
185 | activesupport (>= 5.0.0)
186 | minitest
187 | nokogiri (>= 1.6)
188 | rails-html-sanitizer (1.6.2)
189 | loofah (~> 2.21)
190 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
191 | railties (8.1.1)
192 | actionpack (= 8.1.1)
193 | activesupport (= 8.1.1)
194 | irb (~> 1.13)
195 | rackup (>= 1.0.0)
196 | rake (>= 12.2)
197 | thor (~> 1.0, >= 1.2.2)
198 | tsort (>= 0.2)
199 | zeitwerk (~> 2.6)
200 | rake (13.3.1)
201 | rdoc (6.15.1)
202 | erb
203 | psych (>= 4.0.0)
204 | tsort
205 | reline (0.6.2)
206 | io-console (~> 0.5)
207 | rspec (3.13.2)
208 | rspec-core (~> 3.13.0)
209 | rspec-expectations (~> 3.13.0)
210 | rspec-mocks (~> 3.13.0)
211 | rspec-core (3.13.6)
212 | rspec-support (~> 3.13.0)
213 | rspec-expectations (3.13.5)
214 | diff-lcs (>= 1.2.0, < 2.0)
215 | rspec-support (~> 3.13.0)
216 | rspec-mocks (3.13.7)
217 | diff-lcs (>= 1.2.0, < 2.0)
218 | rspec-support (~> 3.13.0)
219 | rspec-support (3.13.6)
220 | securerandom (0.4.1)
221 | singleton (0.3.0)
222 | slim (5.2.1)
223 | temple (~> 0.10.0)
224 | tilt (>= 2.1.0)
225 | sqlite3 (2.7.4-arm64-darwin)
226 | sqlite3 (2.7.4-x86_64-linux-gnu)
227 | stringio (3.1.7)
228 | temple (0.10.4)
229 | text (1.3.1)
230 | thor (1.4.0)
231 | tilt (2.6.1)
232 | timeout (0.4.4)
233 | tsort (0.2.0)
234 | tzinfo (2.0.6)
235 | concurrent-ruby (~> 1.0)
236 | uri (1.1.1)
237 | useragent (0.16.11)
238 | websocket-driver (0.8.0)
239 | base64
240 | websocket-extensions (>= 0.1.0)
241 | websocket-extensions (0.1.5)
242 | zeitwerk (2.7.3)
243 |
244 | PLATFORMS
245 | arm64-darwin-24
246 | x86_64-linux
247 |
248 | DEPENDENCIES
249 | bump
250 | gettext
251 | gettext_i18n_rails!
252 | haml
253 | hamlit
254 | rails (~> 8.0)
255 | rake
256 | rspec
257 | slim
258 | sqlite3
259 |
260 | BUNDLED WITH
261 | 2.4.13
262 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | gettext_i18n_rails (2.1.0)
5 | fast_gettext (>= 0.9.0)
6 |
7 | GEM
8 | remote: https://rubygems.org/
9 | specs:
10 | action_text-trix (2.1.15)
11 | railties
12 | actioncable (8.1.1)
13 | actionpack (= 8.1.1)
14 | activesupport (= 8.1.1)
15 | nio4r (~> 2.0)
16 | websocket-driver (>= 0.6.1)
17 | zeitwerk (~> 2.6)
18 | actionmailbox (8.1.1)
19 | actionpack (= 8.1.1)
20 | activejob (= 8.1.1)
21 | activerecord (= 8.1.1)
22 | activestorage (= 8.1.1)
23 | activesupport (= 8.1.1)
24 | mail (>= 2.8.0)
25 | actionmailer (8.1.1)
26 | actionpack (= 8.1.1)
27 | actionview (= 8.1.1)
28 | activejob (= 8.1.1)
29 | activesupport (= 8.1.1)
30 | mail (>= 2.8.0)
31 | rails-dom-testing (~> 2.2)
32 | actionpack (8.1.1)
33 | actionview (= 8.1.1)
34 | activesupport (= 8.1.1)
35 | nokogiri (>= 1.8.5)
36 | rack (>= 2.2.4)
37 | rack-session (>= 1.0.1)
38 | rack-test (>= 0.6.3)
39 | rails-dom-testing (~> 2.2)
40 | rails-html-sanitizer (~> 1.6)
41 | useragent (~> 0.16)
42 | actiontext (8.1.1)
43 | action_text-trix (~> 2.1.15)
44 | actionpack (= 8.1.1)
45 | activerecord (= 8.1.1)
46 | activestorage (= 8.1.1)
47 | activesupport (= 8.1.1)
48 | globalid (>= 0.6.0)
49 | nokogiri (>= 1.8.5)
50 | actionview (8.1.1)
51 | activesupport (= 8.1.1)
52 | builder (~> 3.1)
53 | erubi (~> 1.11)
54 | rails-dom-testing (~> 2.2)
55 | rails-html-sanitizer (~> 1.6)
56 | activejob (8.1.1)
57 | activesupport (= 8.1.1)
58 | globalid (>= 0.3.6)
59 | activemodel (8.1.1)
60 | activesupport (= 8.1.1)
61 | activerecord (8.1.1)
62 | activemodel (= 8.1.1)
63 | activesupport (= 8.1.1)
64 | timeout (>= 0.4.0)
65 | activestorage (8.1.1)
66 | actionpack (= 8.1.1)
67 | activejob (= 8.1.1)
68 | activerecord (= 8.1.1)
69 | activesupport (= 8.1.1)
70 | marcel (~> 1.0)
71 | activesupport (8.1.1)
72 | base64
73 | bigdecimal
74 | concurrent-ruby (~> 1.0, >= 1.3.1)
75 | connection_pool (>= 2.2.5)
76 | drb
77 | i18n (>= 1.6, < 2)
78 | json
79 | logger (>= 1.4.2)
80 | minitest (>= 5.1)
81 | securerandom (>= 0.3)
82 | tzinfo (~> 2.0, >= 2.0.5)
83 | uri (>= 0.13.1)
84 | base64 (0.3.0)
85 | bigdecimal (3.3.1)
86 | builder (3.3.0)
87 | bump (0.10.0)
88 | concurrent-ruby (1.3.5)
89 | connection_pool (2.5.4)
90 | crass (1.0.6)
91 | date (3.5.0)
92 | diff-lcs (1.6.2)
93 | drb (2.2.3)
94 | erb (5.1.3)
95 | erubi (1.13.1)
96 | fast_gettext (4.1.1)
97 | prime
98 | racc
99 | forwardable (1.3.3)
100 | gettext (3.5.1)
101 | erubi
102 | locale (>= 2.0.5)
103 | prime
104 | racc
105 | text (>= 1.3.0)
106 | globalid (1.3.0)
107 | activesupport (>= 6.1)
108 | haml (7.0.1)
109 | temple (>= 0.8.2)
110 | thor
111 | tilt
112 | hamlit (4.0.0)
113 | temple (>= 0.8.2)
114 | thor
115 | tilt
116 | i18n (1.14.7)
117 | concurrent-ruby (~> 1.0)
118 | io-console (0.8.1)
119 | irb (1.15.3)
120 | pp (>= 0.6.0)
121 | rdoc (>= 4.0.0)
122 | reline (>= 0.4.2)
123 | json (2.15.2)
124 | locale (2.1.4)
125 | logger (1.7.0)
126 | loofah (2.24.1)
127 | crass (~> 1.0.2)
128 | nokogiri (>= 1.12.0)
129 | mail (2.9.0)
130 | logger
131 | mini_mime (>= 0.1.1)
132 | net-imap
133 | net-pop
134 | net-smtp
135 | marcel (1.1.0)
136 | mini_mime (1.1.5)
137 | minitest (5.26.0)
138 | net-imap (0.5.12)
139 | date
140 | net-protocol
141 | net-pop (0.1.2)
142 | net-protocol
143 | net-protocol (0.2.2)
144 | timeout
145 | net-smtp (0.5.1)
146 | net-protocol
147 | nio4r (2.7.5)
148 | nokogiri (1.18.10-arm64-darwin)
149 | racc (~> 1.4)
150 | nokogiri (1.18.10-x86_64-darwin)
151 | racc (~> 1.4)
152 | nokogiri (1.18.10-x86_64-linux-gnu)
153 | racc (~> 1.4)
154 | pp (0.6.3)
155 | prettyprint
156 | prettyprint (0.2.0)
157 | prime (0.1.4)
158 | forwardable
159 | singleton
160 | psych (5.2.6)
161 | date
162 | stringio
163 | racc (1.8.1)
164 | rack (3.2.4)
165 | rack-session (2.1.1)
166 | base64 (>= 0.1.0)
167 | rack (>= 3.0.0)
168 | rack-test (2.2.0)
169 | rack (>= 1.3)
170 | rackup (2.2.1)
171 | rack (>= 3)
172 | rails (8.1.1)
173 | actioncable (= 8.1.1)
174 | actionmailbox (= 8.1.1)
175 | actionmailer (= 8.1.1)
176 | actionpack (= 8.1.1)
177 | actiontext (= 8.1.1)
178 | actionview (= 8.1.1)
179 | activejob (= 8.1.1)
180 | activemodel (= 8.1.1)
181 | activerecord (= 8.1.1)
182 | activestorage (= 8.1.1)
183 | activesupport (= 8.1.1)
184 | bundler (>= 1.15.0)
185 | railties (= 8.1.1)
186 | rails-dom-testing (2.3.0)
187 | activesupport (>= 5.0.0)
188 | minitest
189 | nokogiri (>= 1.6)
190 | rails-html-sanitizer (1.6.2)
191 | loofah (~> 2.21)
192 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
193 | railties (8.1.1)
194 | actionpack (= 8.1.1)
195 | activesupport (= 8.1.1)
196 | irb (~> 1.13)
197 | rackup (>= 1.0.0)
198 | rake (>= 12.2)
199 | thor (~> 1.0, >= 1.2.2)
200 | tsort (>= 0.2)
201 | zeitwerk (~> 2.6)
202 | rake (13.3.1)
203 | rdoc (6.15.1)
204 | erb
205 | psych (>= 4.0.0)
206 | tsort
207 | reline (0.6.2)
208 | io-console (~> 0.5)
209 | rspec (3.13.2)
210 | rspec-core (~> 3.13.0)
211 | rspec-expectations (~> 3.13.0)
212 | rspec-mocks (~> 3.13.0)
213 | rspec-core (3.13.6)
214 | rspec-support (~> 3.13.0)
215 | rspec-expectations (3.13.5)
216 | diff-lcs (>= 1.2.0, < 2.0)
217 | rspec-support (~> 3.13.0)
218 | rspec-mocks (3.13.7)
219 | diff-lcs (>= 1.2.0, < 2.0)
220 | rspec-support (~> 3.13.0)
221 | rspec-support (3.13.6)
222 | securerandom (0.4.1)
223 | singleton (0.3.0)
224 | slim (5.2.1)
225 | temple (~> 0.10.0)
226 | tilt (>= 2.1.0)
227 | sqlite3 (2.7.4-arm64-darwin)
228 | sqlite3 (2.7.4-x86_64-darwin)
229 | sqlite3 (2.7.4-x86_64-linux-gnu)
230 | stringio (3.1.7)
231 | temple (0.10.4)
232 | text (1.3.1)
233 | thor (1.4.0)
234 | tilt (2.6.1)
235 | timeout (0.4.4)
236 | tsort (0.2.0)
237 | tzinfo (2.0.6)
238 | concurrent-ruby (~> 1.0)
239 | uri (1.1.1)
240 | useragent (0.16.11)
241 | websocket-driver (0.8.0)
242 | base64
243 | websocket-extensions (>= 0.1.0)
244 | websocket-extensions (0.1.5)
245 | zeitwerk (2.7.3)
246 |
247 | PLATFORMS
248 | arm64-darwin-21
249 | arm64-darwin-23
250 | arm64-darwin-24
251 | arm64-darwin-25
252 | x86_64-darwin-22
253 | x86_64-linux
254 |
255 | DEPENDENCIES
256 | bump
257 | gettext
258 | gettext_i18n_rails!
259 | haml
260 | hamlit
261 | rails
262 | rake
263 | rspec
264 | slim
265 | sqlite3
266 |
267 | BUNDLED WITH
268 | 2.4.13
269 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | [FastGettext](http://github.com/grosser/fast_gettext) / Rails integration.
2 |
3 | Translate via FastGettext, use any other I18n backend as extension/fallback.
4 |
5 | Rails does: `I18n.t('syntax.with.lots.of.dots')` with nested yml files
6 | We do: `_('Just translate my damn text!')` with simple, flat mo/po/yml files or directly from db
7 | To use I18n calls add a `syntax.with.lots.of.dots` translation.
8 |
9 | [See it working in the example application.](https://github.com/grosser/gettext_i18n_rails_example)
10 |
11 | Setup
12 | =====
13 | ### Installation
14 |
15 | ```Ruby
16 | # Gemfile
17 | gem 'gettext_i18n_rails'
18 | ```
19 |
20 | ##### Optional:
21 | Add `gettext` if you want to find translations or build .mo files
22 |
23 | ```Ruby
24 | # Gemfile
25 | gem 'gettext', '>=3.0.2', :require => false
26 | ```
27 |
28 | ###### Add first language:
29 | Add the first language using:
30 |
31 | ```Bash
32 | rake gettext:add_language[xx]
33 | ```
34 |
35 | or
36 |
37 | ```Bash
38 | LANGUAGE=xx rake gettext:add_language
39 | ```
40 |
41 | where `xx` is the lowercased [ISO 639-1](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) 2-letter code for the language you want to create.
42 |
43 | for example:
44 |
45 | ```Bash
46 | rake gettext:add_language[es]
47 | ```
48 |
49 |
50 | This will also create the `locales` directory (where the translations are being stored) and run `gettext:find` to find any strings marked for translation.
51 |
52 | You can, of course, add more languages using the same command.
53 |
54 | ### Locales & initialisation
55 | Copy default locales with dates/sentence-connectors/AR-errors you want from e.g.
56 | [rails i18n](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale/) into 'config/locales'
57 |
58 | To initialize:
59 |
60 | ```Ruby
61 | # config/initializers/fast_gettext.rb
62 | FastGettext.add_text_domain 'app', :path => 'locale', :type => :po
63 | FastGettext.default_available_locales = ['en','de'] #all you want to allow
64 | FastGettext.default_text_domain = 'app'
65 | ```
66 |
67 | And in your application:
68 |
69 | ```Ruby
70 | # app/controllers/application_controller.rb
71 | class ApplicationController < ...
72 | before_action :set_gettext_locale
73 | ```
74 |
75 | Translating
76 | ===========
77 | Performance is almost the same for all backends since translations are cached after first use.
78 |
79 | ### Option A: .po files
80 |
81 | ```Ruby
82 | FastGettext.add_text_domain 'app', :path => 'locale', :type => :po
83 | ```
84 |
85 | - use some `_('translations')`
86 | - run `rake gettext:find`, to let GetText find all translations used
87 | - (optional) run `rake gettext:store_model_attributes`, to parse the database for columns that can be translated
88 | - if this is your first translation: `cp locale/app.pot locale/de/app.po` for every locale you want to use
89 | - translate messages in 'locale/de/app.po' (leave msgstr blank and msgstr == msgid)
90 |
91 | New translations will be marked "fuzzy", search for this and remove it, so that they will be used.
92 | Obsolete translations are marked with ~#, they usually can be removed since they are no longer needed
93 |
94 | #### Unfound translations with rake gettext:find
95 | Dynamic translations like `_("x"+"u")` cannot be found. You have 4 options:
96 |
97 | - add `N_('xu')` somewhere else in the code, so the parser sees it
98 | - add `N_('xu')` in a totally separate file like `locale/unfound_translations.rb`, so the parser sees it
99 | - use the [gettext_test_log rails plugin ](http://github.com/grosser/gettext_test_log) to find all translations that where used while testing
100 | - add a Logger to a translation Chain, so every unfound translations is logged ([example](http://github.com/grosser/fast_gettext))
101 |
102 | ### Option B: Traditional .po/.mo files
103 |
104 | FastGettext.add_text_domain 'app', :path => 'locale'
105 |
106 | - follow Option A
107 | - run `rake gettext:pack` to write binary GetText .mo files
108 |
109 | ### Option C: Database
110 | Most scalable method, all translators can work simultaneously and online.
111 |
112 | Easiest to use with the [translation database Rails engine](http://github.com/grosser/translation_db_engine).
113 | Translations can be edited under `/translation_keys`
114 |
115 | ```Ruby
116 | FastGettext::TranslationRepository::Db.require_models
117 | FastGettext.add_text_domain 'app', :type => :db, :model => TranslationKey
118 | ```
119 |
120 | I18n
121 | ====
122 |
123 | ```Ruby
124 | I18n.locale <==> FastGettext.locale.to_sym
125 | I18n.locale = :de <==> FastGettext.locale = 'de'
126 | ```
127 |
128 | Any call to I18n that matches a gettext key will be translated through FastGettext.
129 |
130 | Namespaces
131 | ==========
132 | Car|Model means Model in namespace Car.
133 | You do not have to translate this into english "Model", if you use the
134 | namespace-aware translation
135 |
136 | ```Ruby
137 | s_('Car|Model') == 'Model' #when no translation was found
138 | ```
139 |
140 | XSS / html_safe
141 | ===============
142 | If you trust your translators and all your usages of % on translations:
143 |
144 | ```Ruby
145 | # config/environment.rb
146 | GettextI18nRails.translations_are_html_safe = true
147 | ```
148 |
149 | String % vs html_safe is buggy
150 | My recommended fix is: `require 'gettext_i18n_rails/string_interpolate_fix'`
151 |
152 | - safe stays safe (escape added strings)
153 | - unsafe stays unsafe (do not escape added strings)
154 |
155 | ActiveRecord - error messages
156 | =============================
157 | ActiveRecord error messages are translated through Rails::I18n, but
158 | model names and model attributes are translated through FastGettext.
159 | Therefore a validation error on a BigCar's wheels_size needs `_('big car')` and `_('BigCar|Wheels size')`
160 | to display localized.
161 |
162 | The model/attribute translations can be found through `rake gettext:store_model_attributes`,
163 | (which ignores some commonly untranslated columns like id,type,xxx_count,...).
164 |
165 | Error messages can be translated through FastGettext, if the ':message' is a translation-id or the matching Rails I18n key is translated.
166 |
167 | #### Option A:
168 | Define a translation for "I need my rating!" and use it as message.
169 |
170 | ```Ruby
171 | validates_inclusion_of :rating, :in=>1..5, :message=>N_('I need my rating!')
172 | ```
173 |
174 | #### Option B:
175 |
176 | ```Ruby
177 | validates_inclusion_of :rating, :in=>1..5
178 | ```
179 | Make a translation for the I18n key: `activerecord.errors.models.rating.attributes.rating.inclusion`
180 |
181 | #### Option C:
182 | Add a translation to each config/locales/*.yml files
183 | ```Yaml
184 | en:
185 | activerecord:
186 | errors:
187 | models:
188 | rating:
189 | attributes:
190 | rating:
191 | inclusion: " -- please choose!"
192 | ```
193 | The [rails I18n guide](http://guides.rubyonrails.org/i18n.html) can help with Option B and C.
194 |
195 | Plurals
196 | =======
197 | FastGettext supports pluralization
198 | ```Ruby
199 | n_('Apple','Apples',3) == 'Apples'
200 | ```
201 |
202 | Languages with complex plural forms (such as Polish with its 4 different forms) can also be addressed, see [FastGettext Readme](http://github.com/grosser/fast_gettext)
203 |
204 | Customizing list of translatable files
205 | ======================================
206 | When you run
207 |
208 | ```Bash
209 | rake gettext:find
210 | ```
211 |
212 | by default the following files are going to be scanned for translations: {app,lib,config,locale}/**/*.{rb,erb,haml,slim}. If
213 | you want to specify a different list, you can redefine files_to_translate in the gettext namespace in a file like
214 | lib/tasks/gettext.rake:
215 |
216 | ```Ruby
217 | namespace :gettext do
218 | def files_to_translate
219 | Dir.glob("{app,lib,config,locale}/**/*.{rb,erb,haml,slim,rhtml}")
220 | end
221 | end
222 | ```
223 |
224 | Customizing text domains setup task
225 | ===================================
226 |
227 | By default a single application text domain is created (named `app` or if you load the environment the value of `FastGettext.text_domain` is being used).
228 |
229 | If you want to have multiple text domains or change the definition of the text domains in any way, you can do so by overriding the `:setup` task in a file like lib/tasks/gettext.rake:
230 |
231 | ```Ruby
232 | # Remove the provided gettext setup task
233 | Rake::Task["gettext:setup"].clear
234 |
235 | namespace :gettext do
236 | task :setup => [:environment] do
237 | domains = Application.config.gettext["domains"]
238 |
239 | domains.each do |domain, options|
240 | files = Dir.glob(options["paths"])
241 |
242 | GetText::Tools::Task.define do |task|
243 | task.package_name = options["name"]
244 | task.package_version = "1.0.0"
245 | task.domain = options["name"]
246 | task.po_base_directory = locale_path
247 | task.mo_base_directory = locale_path
248 | task.files = files
249 | task.enable_description = false
250 | task.msgmerge_options = gettext_msgmerge_options
251 | task.msgcat_options = gettext_msgcat_options
252 | task.xgettext_options = gettext_xgettext_options
253 | end
254 | end
255 | end
256 | end
257 | ```
258 |
259 | Changing msgmerge, msgcat, and xgettext options
260 | ===============================================
261 |
262 | The default options for parsing and create `.po` files are:
263 |
264 | ```Bash
265 | --sort-by-msgid --no-location --no-wrap
266 | ```
267 |
268 | These options sort the translations by the msgid (original / source string), don't add location information in the po file and don't wrap long message lines into several lines.
269 |
270 | If you want to override them you can put the following into an initializer like config/initializers/gettext.rb:
271 |
272 | ```Ruby
273 | Rails.application.config.gettext_i18n_rails.msgmerge = %w[--no-location]
274 | Rails.application.config.gettext_i18n_rails.msgcat = %w[--no-location]
275 | Rails.application.config.gettext_i18n_rails.xgettext = %w[--no-location]
276 | ```
277 |
278 | or
279 |
280 | ```Ruby
281 | Rails.application.config.gettext_i18n_rails.default_options = %w[--no-location]
282 | ```
283 |
284 | to override both.
285 |
286 | You can see the available options by running `rgettext -h`, `rmsgcat -f` and `rxgettext -h`.
287 |
288 | Use I18n instead Gettext to ActiveRecord/ActiveModel translations
289 | =================================================================
290 |
291 | If you want to disable translations to model name and attributes you can put the following into an initializer like config/initializers/gettext.rb:
292 |
293 | ```Ruby
294 | Rails.application.config.gettext_i18n_rails.use_for_active_record_attributes = false
295 | ```
296 |
297 | And now you can use your I18n yaml files instead.
298 |
299 | Auto-reload translations in development
300 | ========================================
301 |
302 | By default, .po and .mo files are automatically reloaded in development mode when they change, so you don't need to restart the Rails server after editing translations.
303 |
304 | This feature is enabled by default in development. You can configure it in any environment file:
305 |
306 | ```Ruby
307 | # To disable in development
308 | config.gettext_i18n_rails.auto_reload = false
309 |
310 | # To enable in production (not recommended)
311 | config.gettext_i18n_rails.auto_reload = true
312 | ```
313 |
314 | The auto-reload feature uses `ActiveSupport::FileUpdateChecker` to monitor changes to translation files in your `locale/` directory and reloads them only when they've been modified, ensuring minimal performance impact.
315 |
316 | Using your translations from javascript
317 | =======================================
318 |
319 | If want to use your .PO files on client side javascript you should have a look at the [GettextI18nRailsJs](https://github.com/nubis/gettext_i18n_rails_js) extension.
320 |
321 | [Contributors](http://github.com/grosser/gettext_i18n_rails/contributors)
322 | ======
323 | - [ruby gettext extractor](http://github.com/retoo/ruby_gettext_extractor/tree/master) from [retoo](http://github.com/retoo)
324 | - [Paul McMahon](http://github.com/pwim)
325 | - [Duncan Mac-Vicar P](http://duncan.mac-vicar.com/blog)
326 | - [Ramihajamalala Hery](http://my.rails-royce.org)
327 | - [J. Pablo Fernández](http://pupeno.com)
328 | - [Anh Hai Trinh](http://blog.onideas.ws)
329 | - [ed0h](http://github.com/ed0h)
330 | - [Nikos Dimitrakopoulos](http://blog.nikosd.com)
331 | - [Ben Tucker](http://btucker.net/)
332 | - [Kamil Śliwak](https://github.com/cameel)
333 | - [Rainux Luo](https://github.com/rainux)
334 | - [Lucas Hills](https://github.com/2potatocakes)
335 | - [Ladislav Slezák](https://github.com/lslezak)
336 | - [Greg Weber](https://github.com/gregwebs)
337 | - [Sean Kirby](https://github.com/sskirby)
338 | - [Julien Letessier](https://github.com/mezis)
339 | - [Seb Bacon](https://github.com/sebbacon)
340 | - [Ramón Cahenzli](https://github.com/psy-q)
341 | - [rustygeldmacher](https://github.com/rustygeldmacher)
342 | - [Jeroen Knoops](https://github.com/JeroenKnoops)
343 | - [Ivan Necas](https://github.com/iNecas)
344 | - [Andrey Chernih](https://github.com/AndreyChernyh)
345 | - [Imre Farkas](https://github.com/ifarkas)
346 | - [Trong Tran](https://github.com/trongrg)
347 | - [Dmitri Dolguikh](https://github.com/witlessbird)
348 | - [Joe Ferris](https://github.com/jferris)
349 | - [exAspArk](https://github.com/exAspArk)
350 | - [martinpovolny](https://github.com/martinpovolny)
351 | - [akimd](https://github.com/akimd)
352 | - [adam-h](https://github.com/adam-h)
353 |
354 | [Michael Grosser](http://grosser.it)
355 | michael@grosser.it
356 | License: MIT
357 | [](https://travis-ci.org/grosser/gettext_i18n_rails)
358 |
--------------------------------------------------------------------------------