diff --git a/lib/money/formatting.rb b/lib/money/formatting.rb index 9c4df8f58..9d6152fe6 100644 --- a/lib/money/formatting.rb +++ b/lib/money/formatting.rb @@ -1,6 +1,14 @@ module Money::Formatting include ActiveSupport::NumberHelper + # Locale groups by formatting pattern + # European style: dot as thousands delimiter, comma as decimal separator, symbol after number + EUROPEAN_SYMBOL_AFTER = %i[de es it tr ca ro].freeze + # Scandinavian/Eastern European: space as thousands delimiter, comma as decimal separator, symbol after number + SPACE_DELIMITER_SYMBOL_AFTER = %i[pl nb].freeze + # European style: dot as thousands delimiter, comma as decimal separator, symbol before number + EUROPEAN_SYMBOL_BEFORE = %i[nl pt-BR].freeze + def format(options = {}) locale = options[:locale] || I18n.locale default_opts = format_options(locale) @@ -33,34 +41,29 @@ module Money::Formatting def locale_options(locale) locale_sym = (locale || I18n.locale || :en).to_sym - # French locale: symbol after number with non-breaking space, comma as decimal separator + # French locale: uses non-breaking spaces (unique formatting) if locale_sym == :fr return { delimiter: "\u00A0", separator: ",", format: "%n\u00A0%u" } end - # German locale: symbol after number with space, comma as decimal separator - if locale_sym == :de + # European style: dot delimiter, comma separator, symbol after number + if EUROPEAN_SYMBOL_AFTER.include?(locale_sym) return { delimiter: ".", separator: ",", format: "%n %u" } end - # Spanish locale: symbol after number with space, comma as decimal separator - if locale_sym == :es - return { delimiter: ".", separator: ",", format: "%n %u" } + # Space delimiter, comma separator, symbol after number + if SPACE_DELIMITER_SYMBOL_AFTER.include?(locale_sym) + return { delimiter: " ", separator: ",", format: "%n %u" } end - # Italian locale: symbol after number with space, comma as decimal separator - if locale_sym == :it - return { delimiter: ".", separator: ",", format: "%n %u" } - end - - # Portuguese (Brazil) locale: symbol before, comma as decimal separator - if locale_sym == :"pt-BR" + # European style: dot delimiter, comma separator, symbol before number + if EUROPEAN_SYMBOL_BEFORE.include?(locale_sym) return { delimiter: ".", separator: ",", format: "%u %n" } end # Currency-specific overrides for remaining locales case [ currency.iso_code, locale_sym ] - when [ "EUR", :nl ], [ "EUR", :pt ] + when [ "EUR", :pt ] { delimiter: ".", separator: ",", format: "%u %n" } when [ "EUR", :en ], [ "EUR", :en_IE ] { delimiter: ",", separator: "." } diff --git a/test/lib/money_test.rb b/test/lib/money_test.rb index 7acf814c9..1a43c8e8e 100644 --- a/test/lib/money_test.rb +++ b/test/lib/money_test.rb @@ -113,6 +113,67 @@ class MoneyTest < ActiveSupport::TestCase assert_equal "R$ 1.000,12", Money.new(1000.12, :brl).format(locale: :"pt-BR") end + test "formats correctly for Polish locale" do + # Polish uses space as thousands delimiter, comma as decimal separator, symbol after number + assert_equal "1 000,12 zł", Money.new(1000.12, :pln).format(locale: :pl) + assert_equal "1 000,12 €", Money.new(1000.12, :eur).format(locale: :pl) + end + + test "formats correctly for Turkish locale" do + # Turkish uses dot as thousands delimiter, comma as decimal separator, symbol after number + assert_equal "1.000,12 ₺", Money.new(1000.12, :try).format(locale: :tr) + assert_equal "1.000,12 €", Money.new(1000.12, :eur).format(locale: :tr) + end + + test "formats correctly for Norwegian Bokmål locale" do + # Norwegian uses space as thousands delimiter, comma as decimal separator, symbol after number + assert_equal "1 000,12 kr", Money.new(1000.12, :nok).format(locale: :nb) + assert_equal "1 000,12 €", Money.new(1000.12, :eur).format(locale: :nb) + end + + test "formats correctly for Catalan locale" do + # Catalan uses dot as thousands delimiter, comma as decimal separator, symbol after number + assert_equal "1.000,12 €", Money.new(1000.12, :eur).format(locale: :ca) + end + + test "formats correctly for Romanian locale" do + # Romanian uses dot as thousands delimiter, comma as decimal separator, symbol after number + assert_equal "1.000,12 Lei", Money.new(1000.12, :ron).format(locale: :ro) + assert_equal "1.000,12 €", Money.new(1000.12, :eur).format(locale: :ro) + end + + test "formats correctly for Dutch locale" do + # Dutch uses dot as thousands delimiter, comma as decimal separator, symbol before number + assert_equal "€ 1.000,12", Money.new(1000.12, :eur).format(locale: :nl) + assert_equal "$ 1.000,12", Money.new(1000.12, :usd).format(locale: :nl) + end + + test "formats correctly for Chinese Simplified locale" do + # Chinese Simplified uses English-style formatting (comma as thousands delimiter, dot as decimal separator) + assert_equal "¥1,000.12", Money.new(1000.12, :cny).format(locale: :"zh-CN") + end + + test "formats correctly for Chinese Traditional locale" do + # Chinese Traditional uses English-style formatting (comma as thousands delimiter, dot as decimal separator) + # TWD symbol is prefixed with "TW" to distinguish from other dollar currencies + assert_equal "TW$1,000.12", Money.new(1000.12, :twd).format(locale: :"zh-TW") + end + + test "all supported locales can format money without errors" do + # Ensure all supported locales from LanguagesHelper::SUPPORTED_LOCALES work + supported_locales = %w[en fr de es tr nb ca ro pt-BR zh-CN zh-TW nl] + + supported_locales.each do |locale| + locale_sym = locale.to_sym + # Format with USD and EUR to ensure locale handling works for different currencies + result_usd = Money.new(1000.12, :usd).format(locale: locale_sym) + result_eur = Money.new(1000.12, :eur).format(locale: locale_sym) + + assert result_usd.present?, "Locale #{locale} should format USD without errors" + assert result_eur.present?, "Locale #{locale} should format EUR without errors" + end + end + test "converts currency when rate available" do ExchangeRate.expects(:find_or_fetch_rate).returns(OpenStruct.new(rate: 1.2))