├── .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 | [![Build Status](https://travis-ci.org/grosser/gettext_i18n_rails.png)](https://travis-ci.org/grosser/gettext_i18n_rails) 358 | --------------------------------------------------------------------------------