From c5fb8d73cc1a4305a527ac3b77641cd99d65486f Mon Sep 17 00:00:00 2001 From: Kenta Ishizaki <153918146+55728@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:36:25 +0900 Subject: [PATCH] fix: Prepend UTF-8 BOM to contact CSV export for non-ASCII character support (#14123) ## Description Spreadsheet applications such as Microsoft Excel do not auto-detect UTF-8 encoding when opening CSV files. This causes non-ASCII characters (Arabic, Japanese, Chinese, Korean, etc.) to appear garbled in the exported contacts CSV. This PR prepends the UTF-8 Byte Order Mark (`EF BB BF`) to the CSV output in `Account::ContactsExportJob`, which signals to spreadsheet applications that the file is UTF-8 encoded. Fixes: #13998 --- app/jobs/account/contacts_export_job.rb | 7 ++++++- spec/jobs/account/contacts_export_job_spec.rb | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/jobs/account/contacts_export_job.rb b/app/jobs/account/contacts_export_job.rb index 952795604..a66928a79 100644 --- a/app/jobs/account/contacts_export_job.rb +++ b/app/jobs/account/contacts_export_job.rb @@ -42,8 +42,13 @@ class Account::ContactsExportJob < ApplicationJob def attach_export_file(csv_data) return if csv_data.blank? + # Prepend UTF-8 BOM so that spreadsheet applications (e.g. Excel) + # correctly recognise the file encoding for non-ASCII characters + # such as Arabic, Japanese, and Chinese. + bom = "\xEF\xBB\xBF" + @account.contacts_export.attach( - io: StringIO.new(csv_data), + io: StringIO.new("#{bom}#{csv_data}"), filename: "#{@account.name}_#{@account.id}_contacts.csv", content_type: 'text/csv' ) diff --git a/spec/jobs/account/contacts_export_job_spec.rb b/spec/jobs/account/contacts_export_job_spec.rb index 9b9d74675..561015ea7 100644 --- a/spec/jobs/account/contacts_export_job_spec.rb +++ b/spec/jobs/account/contacts_export_job_spec.rb @@ -85,6 +85,13 @@ RSpec.describe Account::ContactsExportJob do expect(phone_numbers).to include('+910808080818', '+910808080808') end + it 'prepends UTF-8 BOM to the exported CSV for spreadsheet compatibility' do + described_class.perform_now(account.id, user.id, [], {}) + + raw = account.contacts_export.download + expect(raw.bytes[0..2]).to eq([0xEF, 0xBB, 0xBF]) + end + it 'returns all resolved contacts as results when filter is not prvoided' do create(:contact, account: account, email: nil, phone_number: nil) described_class.perform_now(account.id, user.id, %w[id name email column_not_present], {})