{"id":8597,"date":"2026-11-02T06:00:00","date_gmt":"2026-11-02T06:00:00","guid":{"rendered":"https:\/\/poeditor.com\/blog\/?p=8597"},"modified":"2026-10-30T07:22:49","modified_gmt":"2026-10-30T07:22:49","slug":"rails-i18n","status":"publish","type":"post","link":"https:\/\/poeditor.com\/blog\/rails-i18n\/","title":{"rendered":"Rails i18n: How to translate a Ruby on Rails app with YAML locale files"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"733\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/rails-i18n-1024x733.png\" alt=\"\" class=\"wp-image-8617\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/rails-i18n-1024x733.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/rails-i18n-300x215.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/rails-i18n-767x549.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/rails-i18n-1536x1099.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/rails-i18n.png 1600w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Ruby on Rails is a web framework for Ruby that follows the principle of convention over configuration. A new application comes with its database layer, routing, views and asset handling already set up, so there\u2019s less to configure before you can start building.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As soon as an app serves people in more than one country, they expect to read it in their own language, with dates and numbers written the way they\u2019re used to. Internationalization (i18n) is the work that makes the code ready for that, and localization (l10n) supplies the translations and formats for each language.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In this guide, we\u2019ll translate a Rails TO-DO app into French using Rails\u2019 built-in I18n API, YAML locale files, the rails-i18n gem and POEditor, while keeping English as the default language.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What Rails I18n already does<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The TO-DO app runs on Rails 8.1 and Ruby 4.0, with a SQLite database. You register with a username and password, create lists, add tasks, mark them as done and filter a list by open or completed tasks. Labels, buttons and flash messages are hardcoded in English in the ERB views and controllers. Counts use Rails\u2019 <code>pluralize<\/code> helper or custom singular and plural conditions, and dates are formatted with <code>strftime<\/code> in the US style.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"648\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/01_lists_page_en-1-1024x648.png\" alt=\"\" class=\"wp-image-8605\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/01_lists_page_en-1-1024x648.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/01_lists_page_en-1-300x190.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/01_lists_page_en-1-768x486.png 768w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/01_lists_page_en-1-1536x972.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/01_lists_page_en-1-2048x1296.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>The lists page before translation<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">To follow along you need Ruby 4.0.9, the version the <code>Gemfile<\/code> asks for. Run <code>bundle install<\/code> and <code>bin\/rails db:prepare<\/code>, which creates the database along with a <code>demo<\/code> account whose password is <code>demo1234<\/code>, then start the app with bin\/rails server and open the address it prints.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Rails ships with the <a href=\"https:\/\/github.com\/ruby-i18n\/i18n\" rel=\"nofollow\">i18n gem<\/a> and reads its translations from every <code>.yml<\/code> and <code>.rb<\/code> file under <code>config\/locales<\/code>, subfolders included. Some of the TO-DO app\u2019s text is already translated through Rails\u2019 built-in locale files: validation messages such as \u201cName can\u2019t be blank\u201d come from Rails\u2019 English translations. Rails\u2019 English locale files also hold the month and day names that the <code>l<\/code> helper uses for dates, which the app switches to later. Rails only ships this data in English, so for French you add the <a href=\"https:\/\/github.com\/svenfuchs\/rails-i18n\" rel=\"nofollow\">rails-i18n<\/a> gem, which supplies French validation messages, month names, number formats and the French plural rule.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Add the gem to the Gemfile and run <code>bundle install<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Gemfile\ngem \"rails-i18n\", \"8.1.0\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then tell Rails which languages the app supports and which one it uses by default:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># config\/application.rb\nmodule TodoApp\n&nbsp; class Application &lt; Rails::Application\n&nbsp; &nbsp; # ...\n\n&nbsp; &nbsp; # The languages the app is translated into; English is the default.\n&nbsp; &nbsp; config.i18n.available_locales = &#091; :en, :fr ]\n&nbsp; &nbsp; config.i18n.default_locale = :en\n&nbsp; end\nend<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>available_locales<\/code> also limits what rails-i18n loads. The gem contains data for over a hundred locales, but with this setting it only adds English and French to the app. Restart <code>bin\/rails server<\/code> after these two changes, because Rails doesn\u2019t reload the Gemfile or <code>config\/application.rb<\/code> while it runs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Moving view text into en.yml<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A Rails translation is stored under a key in a YAML file and retrieved with the t helper. You can write the full key, as in <code>t(\"lists.index.title\")<\/code>, but views usually use the shorter form with a leading dot, known as lazy lookup. In <code>app\/views\/lists\/index.html.erb, t(\".title\")<\/code> looks up <code>lists.index.title<\/code>, because the key is built from the template\u2019s folder and file name. Partials drop the underscore, so <code>t(\".tasks_left\")<\/code> in <code>lists\/_list.html.erb<\/code> reads <code>lists.list.tasks_left<\/code>, and the layout\u2019s strings live under <code>layouts.application<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here is the lists page with its strings replaced:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;%# app\/views\/lists\/index.html.erb %&gt;\n&lt;% content_for <strong><em>:title<\/em><\/strong>, t(\".title\") %&gt;\n\n<strong>&lt;div<\/strong> <strong>class<\/strong>=\"card shadow-sm mb-4\"<strong>&gt;<\/strong>\n&nbsp; <strong>&lt;div<\/strong> <strong>class<\/strong>=\"card-body\"<strong>&gt;<\/strong>\n&nbsp; &nbsp; &lt;%= form_with model: @new_list, url: lists_path, id: \"new-list-form\", <strong>class<\/strong>: \"row g-2 align-items-start\" <strong>do<\/strong> |form| %&gt;\n&nbsp; &nbsp; &nbsp; <strong>&lt;div<\/strong> <strong>class<\/strong>=\"col-12 col-sm\"<strong>&gt;<\/strong>\n&nbsp; &nbsp; &nbsp; &nbsp; &lt;%= form.label <strong><em>:name<\/em><\/strong>, t(\".new_list\"), <strong>class<\/strong>: \"form-label\" %&gt;\n&nbsp; &nbsp; &nbsp; &nbsp; &lt;%= form.text_field <strong><em>:name<\/em><\/strong>, placeholder: t(\".placeholder\"),\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <strong>class<\/strong>: class_names(\"form-control\", \"is-invalid\": @new_list.errors.include?(<strong><em>:name<\/em><\/strong>)) %&gt;\n&nbsp; &nbsp; &nbsp; &nbsp; &lt;%= field_errors @new_list, <strong><em>:name<\/em><\/strong> %&gt;\n&nbsp; &nbsp; &nbsp; <strong>&lt;\/div&gt;<\/strong>\n&nbsp; &nbsp; &nbsp; <strong>&lt;div<\/strong> <strong>class<\/strong>=\"col-12 col-sm-auto align-self-sm-end\"<strong>&gt;<\/strong>\n&nbsp; &nbsp; &nbsp; &nbsp; &lt;%= form.submit t(\".create\"), <strong>class<\/strong>: \"btn btn-primary w-100\" %&gt;\n&nbsp; &nbsp; &nbsp; <strong>&lt;\/div&gt;<\/strong>\n&nbsp; &nbsp; &lt;% <strong>end<\/strong> %&gt;\n&nbsp; <strong>&lt;\/div&gt;<\/strong>\n<strong>&lt;\/div&gt;<\/strong>\n\n&lt;% <strong>if<\/strong> @lists.empty? %&gt;\n&nbsp; <strong>&lt;p<\/strong> <strong>class<\/strong>=\"text-body-secondary\"<strong>&gt;<\/strong>&lt;%= t(\".empty\") %&gt;<strong>&lt;\/p&gt;<\/strong>\n&lt;% <strong>else<\/strong> %&gt;\n&nbsp; <strong>&lt;p<\/strong> <strong>class<\/strong>=\"text-body-secondary\"<strong>&gt;<\/strong>&lt;%= t(\".count\", count: @lists.size) %&gt;<strong>&lt;\/p&gt;<\/strong>\n&nbsp; ...\n&lt;% <strong>end<\/strong> %&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>rails new<\/code> creates <code>config\/locales\/en.yml<\/code> with a single <code>hello<\/code> key. Replace it with the app\u2019s own strings, nested by folder, template and key:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em># config\/locales\/en.yml<\/em>\nen<strong>:<\/strong>\n&nbsp; layouts<strong>:<\/strong>\n&nbsp; &nbsp; application<strong>:<\/strong>\n&nbsp; &nbsp; &nbsp; menu<strong>:<\/strong> \"Menu\"\n&nbsp; &nbsp; &nbsp; signed_in_as_html<strong>:<\/strong> \"Signed in as &lt;strong&gt;%{username}&lt;\/strong&gt;\"\n&nbsp; &nbsp; &nbsp; log_out<strong>:<\/strong> \"Log out\"\n&nbsp; &nbsp; &nbsp; log_in<strong>:<\/strong> \"Log in\"\n&nbsp; &nbsp; &nbsp; register<strong>:<\/strong> \"Register\"\n&nbsp; lists<strong>:<\/strong>\n&nbsp; &nbsp; index<strong>:<\/strong>\n&nbsp; &nbsp; &nbsp; title<strong>:<\/strong> \"Your lists\"\n&nbsp; &nbsp; &nbsp; new_list<strong>:<\/strong> \"New list\"\n&nbsp; &nbsp; &nbsp; placeholder<strong>:<\/strong> \"Groceries\"\n&nbsp; &nbsp; &nbsp; create<strong>:<\/strong> \"Create list\"\n&nbsp; &nbsp; &nbsp; empty<strong>:<\/strong> \"You don't have any lists yet. Create your first one above.\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Values that change at runtime go in as <code>%{name}<\/code> placeholders and are passed to t as options, for example <code>t(\".signed_in_as_html\", username: current_user.username)<\/code> in the navbar. A key that ends in <code>_html<\/code> is marked as safe HTML, so the <code>&lt;strong&gt; tag<\/code> reaches the page as markup, while Rails still escapes the username you pass in. Without the suffix, the tag would be escaped and shown as text.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because every string has its own key, the same English word can be translated differently in different places. The TO-DO app shows \u201cDone\u201d both as a filter tab above the tasks and as a badge on each completed task. In French, the tab refers to several tasks and the badge to one, so <code>lists.show.filters.done<\/code> becomes <code>Termin\u00e9es<\/code> and <code>tasks.task.done<\/code> becomes <code>Termin\u00e9e<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Translating flash messages and model attributes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Lazy lookup also works in controllers, where the key is built from the controller and the action. <code>t(\".created\")<\/code> in <code>ListsController#create<\/code> reads <code>lists.create.created<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em># app\/controllers\/lists_controller.rb<\/em>\n<strong>def<\/strong> create\n&nbsp; @new_list = current_user.lists.new(list_params)\n\n&nbsp; <strong>if<\/strong> @new_list.save\n&nbsp; &nbsp; redirect_to @new_list, <strong><em>notice:<\/em><\/strong> t(\".created\", <strong><em>name:<\/em><\/strong> @new_list.name)\n&nbsp; <strong>else<\/strong>\n&nbsp; &nbsp; load_lists\n&nbsp; &nbsp; render <strong><em>:index<\/em><\/strong>, <strong><em>status:<\/em><\/strong> <strong><em>:unprocessable_content<\/em><\/strong>\n&nbsp; <strong>end<\/strong>\n<strong>end<\/strong><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The original app built some of these messages by joining strings, as in <code>\"Task \\\"\" + @task.text + \"\\\" marked as done.\"<\/code>. A translator can\u2019t change the word order when a sentence is split across three pieces of code, so put each message in a single string with a placeholder:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># app\/controllers\/tasks_controller.rb\ndef toggle\n&nbsp; @task.mark_done!(params.expect(:done) == \"1\")\n\n&nbsp; message = @task.done? ? t(\".done\", text: @task.text) : t(\".open\", text: @task.text)\n&nbsp; redirect_to list_path(@list, show: filter_param), notice: message\nend\n\n# config\/locales\/en.yml\n&nbsp; tasks:\n&nbsp; &nbsp; toggle:\n&nbsp; &nbsp; &nbsp; done: \"Task \\\"%{text}\\\" marked as done.\"\n&nbsp; &nbsp; &nbsp; open: \"Task \\\"%{text}\\\" marked as open.\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Validation messages need a different kind of key. Rails builds \u201cText can\u2019t be blank\u201d from the attribute\u2019s name and a message, and it looks for the name under <code>activerecord.attributes<\/code> before falling back to a humanized column name. The <code>tasks.text <\/code>column, for instance, reads better as \u201cTask\u201d:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em># config\/locales\/en.yml<\/em>\n&nbsp; activerecord<strong>:<\/strong>\n&nbsp; &nbsp; attributes<strong>:<\/strong>\n&nbsp; &nbsp; &nbsp; list<strong>:<\/strong>\n&nbsp; &nbsp; &nbsp; &nbsp; name<strong>:<\/strong> \"Name\"\n&nbsp; &nbsp; &nbsp; task<strong>:<\/strong>\n&nbsp; &nbsp; &nbsp; &nbsp; text<strong>:<\/strong> \"Task\"\n&nbsp; &nbsp; &nbsp; user<strong>:<\/strong>\n&nbsp; &nbsp; &nbsp; &nbsp; username<strong>:<\/strong> \"Username\"\n&nbsp; &nbsp; &nbsp; &nbsp; password<strong>:<\/strong> \"Password\"\n&nbsp; &nbsp; &nbsp; &nbsp; password_confirmation<strong>:<\/strong> \"Password confirmation\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The same names label the username and password fields of the registration form, because <code>form.label :username<\/code> without any text of its own looks up the model\u2019s attribute name. You don\u2019t translate the messages themselves: once the page is in French, rails-i18n supplies <code>doit \u00eatre rempli(e)<\/code>, and with <code>Texte de la t\u00e2che<\/code> as the French name of <code>task.text<\/code>, an empty task shows \u201cTexte de la t\u00e2che doit \u00eatre rempli(e)\u201d.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rails-i18n doesn\u2019t cover every message Rails has, though. <code>has_secure_password<\/code> rejects a password longer than 72 bytes with the message <code>errors.messages.password_too_long<\/code>, and rails-i18n 8.1.0 has no French translation for it, so in development a French page shows \u201cTranslation missing\u201d there, and in production the English message. Copy the English message into <code>en.yml<\/code>, and it goes to POEditor with the rest of the strings:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em># config\/locales\/en.yml<\/em>\n<em>&nbsp; # Rails' own message for a password over 72 bytes, which rails-i18n has no French version of yet.<\/em>\n&nbsp; errors<strong>:<\/strong>\n&nbsp; &nbsp; messages<strong>:<\/strong>\n&nbsp; &nbsp; &nbsp; password_too_long<strong>:<\/strong> \"is too long\"<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Plurals with the count option<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The original app counted tasks with <code>pluralize(list.open_count, \"task\")<\/code>, which builds the plural word from Rails\u2019 English inflection rules. In a translated app, pass the number to <code>t<\/code> as the <code>count<\/code> option and add an entry for each plural form under the existing <code>lists<\/code> key in <code>en.yml<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;%# app\/views\/lists\/_list.html.erb %&gt;\n&lt;p class=\"card-text mb-2\"&gt;\n&nbsp; &lt;%= t(\".tasks_left\", count: list.open_count) %&gt;\n&nbsp; &lt;span class=\"text-body-secondary\"&gt;\n&nbsp; &nbsp; &amp;middot;\n&nbsp; &nbsp; &lt;%= t(\".tasks_total\", count: list.total_count) %&gt;\n&nbsp; &lt;\/span&gt;\n&lt;\/p&gt;\n\n# config\/locales\/en.yml\n&nbsp; lists:\n&nbsp; &nbsp; list:\n&nbsp; &nbsp; &nbsp; tasks_left:\n&nbsp; &nbsp; &nbsp; &nbsp; one: \"%{count} task left\"\n&nbsp; &nbsp; &nbsp; &nbsp; other: \"%{count} tasks left\"\n&nbsp; &nbsp; &nbsp; tasks_total:\n&nbsp; &nbsp; &nbsp; &nbsp; one: \"%{count} task in total\"\n&nbsp; &nbsp; &nbsp; &nbsp; other: \"%{count} tasks in total\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>count<\/code> both chooses the form and fills in <code>%{count}<\/code>. Which form it chooses depends on the language\u2019s plural rule. Rails on its own applies the English rule to every language, one for exactly 1 and <code>other<\/code> for everything else, but French uses the singular for 0 as well, and rails-i18n adds that rule. With rails-i18n installed, a finished list reads \u201c0 t\u00e2che restante\u201d instead of \u201c0 t\u00e2ches restantes\u201d. The \u201cYou have 3 lists.\u201d line on the lists page works the same way, with one and other entries under lists.index.count.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The list page has a sentence under its filter tabs that changes with both the filter and the number of tasks, \u201cShowing 5 completed tasks.\u201d for example. The original app picked it with a <code>case<\/code> statement and a singular or plural branch for each filter. Now each filter has a plural key, and the view builds the key name from the current filter:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;%# app\/views\/lists\/show.html.erb %&gt;\n&lt;p class=\"text-body-secondary small mb-3\"&gt;&lt;%= t(\".showing.#{@filter}\", count: @tasks.size) %&gt;&lt;\/p&gt;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Dates, times and percentages<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>l<\/code> helper formats a date or time with a named format from the locale files, and it fills in month and day names in the current language. A list card shows the day the list was created, and each task shows when it was added:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;%# app\/views\/lists\/_list.html.erb %&gt;\n&lt;%= t(\".created_on\", date: l(list.created_at.to_date, format: <strong><em>:long<\/em><\/strong>)) %&gt;\n\n&lt;%# app\/views\/tasks\/_task.html.erb %&gt;\n&lt;%= t(\".added_on\", time: l(task.created_at, format: <strong><em>:short<\/em><\/strong>)) %&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A <code>Date<\/code> uses the formats under date.formats, while a <code>Time<\/code> uses <code>time.formats<\/code>, which is why the list card converts <code>created_at<\/code> with <code>to_date<\/code>. Rails\u2019 own English <code>long<\/code> date format is <code>%B %d, %Y<\/code>, which pads the day to \u201cOctober 09, 2026\u201d, and its time formats use a 24-hour clock. To keep the app\u2019s original output, override the formats in <code>en.yml<\/code>. The app\u2019s locale files load after the ones in Rails and rails-i18n, so the same key replaces theirs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em># config\/locales\/en.yml<\/em>\n&nbsp; date<strong>:<\/strong>\n&nbsp; &nbsp; formats<strong>:<\/strong>\n<em>&nbsp; &nbsp; &nbsp; # October 9, 2026 (strftime: %B month name, %-d day, %Y year)<\/em>\n&nbsp; &nbsp; &nbsp; long<strong>:<\/strong> \"%B %-d, %Y\"\n\n&nbsp; time<strong>:<\/strong>\n&nbsp; &nbsp; formats<strong>:<\/strong>\n<em>&nbsp; &nbsp; &nbsp; # October 9, 2026 at 8:12 AM (strftime: %-I hour on a 12-hour clock, %M minutes, %p AM or PM)<\/em>\n&nbsp; &nbsp; &nbsp; long<strong>:<\/strong> \"%B %-d, %Y at %-I:%M %p\"\n<em>&nbsp; &nbsp; &nbsp; # 10\/09\/2026 at 4:22 PM (strftime: %m month, %d day, %Y year)<\/em>\n&nbsp; &nbsp; &nbsp; short<strong>:<\/strong> \"%m\/%d\/%Y at %-I:%M %p\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">These formats are stored in en.yml like any other translation, so they can be translated along with the rest of the file. In French, the format is <code>%-d %B %Y<\/code>, with the day before the month. The comments explain the <code>strftime<\/code> codes to whoever translates them, and they travel with the file into POEditor. <code>%B<\/code> becomes <code>octobre<\/code> in French because <code>l<\/code> takes the month names from rails-i18n.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The progress text under each list, \u201c83% done\u201d, comes from a translation with a <code>%{percent}<\/code> placeholder, so the translator decides where the number and the percent sign go. French typography puts a space before the percent sign, which makes the French translation <code>%{percent} % termin\u00e9<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Setting the locale from the URL<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rails keeps the current language in <code>I18n.locale<\/code>, and the app has to set it at the start of every request. The Rails guides recommend putting the locale in the URL, so each page has a language-specific address and shared links open in the same language for everyone. Wrap the routes in an optional <code>locale<\/code> segment:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em># config\/routes.rb<\/em>\nRails.application.routes.draw <strong>do<\/strong>\n&nbsp; <em># English pages have no prefix (\/lists\/1), French ones start with \/fr (\/fr\/lists\/1).<\/em>\n&nbsp; scope \"(:locale)\", <strong><em>locale:<\/em><\/strong> \/en|fr\/ <strong>do<\/strong>\n&nbsp; &nbsp; root \"lists#index\"\n\n&nbsp; &nbsp; resource <strong><em>:registration<\/em><\/strong>, <strong><em>only:<\/em><\/strong> %i<strong>&#091;<\/strong>new create<strong>]<\/strong>\n&nbsp; &nbsp; resource <strong><em>:session<\/em><\/strong>, <strong><em>only:<\/em><\/strong> %i<strong>&#091;<\/strong>new create destroy<strong>]<\/strong>\n\n&nbsp; &nbsp; resources <strong><em>:lists<\/em><\/strong>, <strong><em>only:<\/em><\/strong> %i<strong>&#091;<\/strong>index show create update destroy<strong>]<\/strong> <strong>do<\/strong>\n&nbsp; &nbsp; &nbsp; resources <strong><em>:tasks<\/em><\/strong>, <strong><em>only:<\/em><\/strong> %i<strong>&#091;<\/strong>create update destroy<strong>]<\/strong> <strong>do<\/strong>\n&nbsp; &nbsp; &nbsp; &nbsp; patch <strong><em>:toggle<\/em><\/strong>, <strong><em>on:<\/em><\/strong> <strong><em>:member<\/em><\/strong>\n&nbsp; &nbsp; &nbsp; <strong>end<\/strong>\n&nbsp; &nbsp; <strong>end<\/strong>\n&nbsp; <strong>end<\/strong>\n\n&nbsp; get \"up\" =&gt; \"rails\/health#show\", <strong><em>as:<\/em><\/strong> <strong><em>:rails_health_check<\/em><\/strong>\n<strong>end<\/strong><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then set the locale in <code>ApplicationController <\/code>with an <code>around_action<\/code>, which switches the language for the rest of the request and restores the previous one afterwards:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em># app\/controllers\/application_controller.rb<\/em>\n<strong>class<\/strong> ApplicationController &lt; ActionController::Base\n&nbsp; allow_browser <strong><em>versions:<\/em><\/strong> <strong><em>:modern<\/em><\/strong>\n&nbsp; <em># ...<\/em>\n\n&nbsp; <em># Runs before require_login, so everything after it, including require_login's redirect, uses the request's language.<\/em>\n&nbsp; around_action <strong><em>:switch_locale<\/em><\/strong>\n&nbsp; before_action <strong><em>:require_login<\/em><\/strong>\n\n&nbsp; helper_method <strong><em>:current_user<\/em><\/strong>\n\n&nbsp; private\n\n&nbsp; <em># The language comes from the locale in the URL; a missing or unsupported one means English.<\/em>\n&nbsp; <strong>def<\/strong> switch_locale(&amp;action)\n&nbsp; &nbsp; locale = params<strong>&#091;<em>:locale<\/em>]<\/strong> <strong>if<\/strong> I18n.locale_available?(params<strong>&#091;<em>:locale<\/em>]<\/strong>)\n&nbsp; &nbsp; I18n.with_locale(locale || I18n.default_locale, &amp;action)\n&nbsp; <strong>end<\/strong>\n\n&nbsp; <em># Keeps the \/fr prefix on every link and redirect the app generates.<\/em>\n&nbsp; <strong>def<\/strong> default_url_options\n&nbsp; &nbsp; { <strong><em>locale:<\/em><\/strong> (I18n.locale <strong>unless<\/strong> I18n.locale == I18n.default_locale) }\n&nbsp; <strong>end<\/strong>\n\n&nbsp; <em># ...<\/em>\n<strong>end<\/strong><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The route constraint only accepts <code>en<\/code> and <code>fr<\/code> in the path, but params[:locale] can also come from the query string, and by default <code>I18n.with_locale<\/code> raises <code>I18n::InvalidLocale<\/code> for a language that isn\u2019t in available_locales. The <code>locale_available?<\/code> check turns an address like <code>\/?locale=de<\/code> into a normal English page instead of an error. <code>default_url_options<\/code> adds the locale to every URL helper, so once a visitor is on a French page, <code>list_path(list) <\/code>returns <code>\/fr\/lists\/1<\/code> and they stay in French. Returning <code>nil<\/code> for English keeps the English URLs without a prefix.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The language switcher in the navbar links to the current page in each language. Each language name is written in that language, so the names aren\u2019t translated. The partial\u2019s only translated string is its <code>aria-label<\/code>, which goes under the <code>layouts: key<\/code> in <code>en.yml<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;%# app\/views\/layouts\/_language_switcher.html.erb %&gt;\n&lt;nav class=\"btn-group btn-group-sm\" aria-label=\"&lt;%= t(\".label\") %&gt;\"&gt;\n&nbsp; &lt;% { en: \"English\", fr: \"Fran\u00e7ais\" }.each do |locale, name| %&gt;\n&nbsp; &nbsp; &lt;%= link_to name, switch_locale_path(locale), lang: locale, hreflang: locale,\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; class: class_names(\"btn\", locale == I18n.locale ? \"btn-secondary\" : \"btn-outline-secondary\"),\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; aria: { current: (\"true\" if locale == I18n.locale) } %&gt;\n&nbsp; &lt;% end %&gt;\n&lt;\/nav&gt;\n\n# config\/locales\/en.yml\n&nbsp; layouts:\n&nbsp; &nbsp; language_switcher:\n&nbsp; &nbsp; &nbsp; label: \"Language\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The links come from a helper. On an ordinary page, <code>url_for(locale: locale)<\/code> rebuilds the current URL with the other locale, and the list page\u2019s task filter is passed along so it survives the switch. When a form fails validation, Rails renders the page in response to the POST, and a link can\u2019t repeat a POST, so the helper links to the page that holds the form instead:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em># app\/helpers\/application_helper.rb<\/em>\n<em># The current page in another language, with the same task filter. A page<\/em>\n<em># shown after a form failed is the answer to a POST, which a link can't<\/em>\n<em># repeat, so it links to the page that holds the form instead.<\/em>\n<strong>def<\/strong> switch_locale_path(locale)\n&nbsp; locale = nil <strong>if<\/strong> locale == I18n.default_locale\n\n&nbsp; <strong>if<\/strong> request.get?\n&nbsp; &nbsp; url_for(<strong><em>locale:<\/em><\/strong> locale, <strong><em>show:<\/em><\/strong> @show)\n&nbsp; <strong>elsif<\/strong> @list\n&nbsp; &nbsp; list_path(@list, <strong><em>locale:<\/em><\/strong> locale, <strong><em>show:<\/em><\/strong> @show)\n&nbsp; <strong>elsif<\/strong> current_user\n&nbsp; &nbsp; root_path(<strong><em>locale:<\/em><\/strong> locale)\n&nbsp; <strong>else<\/strong>\n&nbsp; &nbsp; url_for(<strong><em>action:<\/em><\/strong> <strong><em>:new<\/em><\/strong>, <strong><em>locale:<\/em><\/strong> locale)\n&nbsp; <strong>end<\/strong>\n<strong>end<\/strong><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In the layout, render the switcher in the navbar with <code>&lt;%= render \"layouts\/language_switcher\" %&gt;<\/code>, and change <code>&lt;html lang=\"en\"&gt; to &lt;html lang=\"&lt;%= I18n.locale %&gt;\"&gt;<\/code>, so browsers and screen readers know which language the page is in.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before French has any translations, <code>\/fr<\/code> shows each missing string in a view as the last part of its key, title-cased, such as \u201cTitle\u201d for <code>lists.index.title<\/code>, inside a span with a <code>translation_missing<\/code> class and a title naming the key it looked for. In a submit button\u2019s label, a placeholder or the browser tab\u2019s title, that markup appears as text, and where a view writes <code>t<\/code> straight into an HTML attribute, such as the switcher\u2019s <code>aria-label<\/code>, it ends the attribute early, so fragments like <code>Label\"&gt;<\/code> show up on the page. This happens in development. In production, a missing French key shows the English text instead, because the <code>production.rb<\/code> that rails new generates sets <code>config.i18n.fallbacks = true<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Translating en.yml in POEditor<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In POEditor, create a project for the app from the <strong>+<\/strong> button on the <strong>Dashboard<\/strong> and name it TO-DO app (Rails). Add English and French as its languages.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"636\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/02_project_languages-1-1024x636.png\" alt=\"\" class=\"wp-image-8608\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/02_project_languages-1-1024x636.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/02_project_languages-1-300x186.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/02_project_languages-1-767x477.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/02_project_languages-1-1536x955.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/02_project_languages-1-2048x1273.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>The project\u2019s languages, before any import<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Open <strong>Import<\/strong> and choose <code>config\/locales\/en.yml<\/code> on the <strong>Import Terms<\/strong> page. On a project\u2019s first import, <strong>Also import translations to a language<\/strong> is already switched on, so leave it on and choose English from the list under it. POEditor recognizes YAML from the <code>.yml<\/code> extension, and <strong>Import to project<\/strong> starts the import.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"975\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/03_import_yml-1024x975.png\" alt=\"\" class=\"wp-image-8609\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/03_import_yml-1024x975.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/03_import_yml-300x286.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/03_import_yml-767x730.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/03_import_yml-1536x1462.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/03_import_yml-2048x1949.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>Importing<\/em> <em>en.yml, with its values going into English<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">POEditor drops the <code>en:<\/code> root, turns each last key into a term and the path above it into the term\u2019s context, so <code>lists.index.title<\/code> arrives as the term <code>title<\/code> with the context <code>\"lists\".\"index\"<\/code>. Plural keys, the ones whose children are <code>one<\/code> and <code>other<\/code>, become a single plural term each, and a comment above a key, such as the ones above the date formats, becomes that term\u2019s comment. A comment above a parent key, such as the one above <code>errors<\/code>:, doesn\u2019t appear on any term, but POEditor keeps it and writes it back into the YAML export. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The TO-DO app\u2019s <code>en.yml<\/code> gives 82 terms, with English at 100%. Since the terms are keys, translators need to see the English text alongside them. The import doesn\u2019t set this automatically. Go back to the project page, click the gear icon to open the project settings, click <strong>Edit Details<\/strong>, set <strong>DEFAULT REFERENCE LANGUAGE<\/strong> to English and click <strong>Save project details<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now open French and translate. Each row shows the English text, with the key and its context in smaller type below it. Keep placeholders such as <code>%{name}<\/code> and <code>%{count}<\/code> as they are, though you can move them within the sentence. If a translation drops or changes one, QA Checks flag it when you save. A plural term lists its English forms, and its translation box has a tab for each French plural form: <strong>ONE<\/strong>, <strong>MANY<\/strong> and <strong>OTHER<\/strong>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"718\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/04_french_translations-1-1024x718.png\" alt=\"\" class=\"wp-image-8610\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/04_french_translations-1-1024x718.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/04_french_translations-1-300x210.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/04_french_translations-1-767x538.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/04_french_translations-1-1536x1077.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/04_french_translations-1-2048x1436.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>French translations, with the plural terms\u2019 ONE, MANY and OTHER tabs<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">French uses the <strong>MANY<\/strong> form for exact multiples of a million, as in \u201c1 000 000 de t\u00e2ches\u201d. The rails-i18n French rule only has <code>one<\/code> and <code>other<\/code>, so Rails doesn\u2019t use that form unless you add your own French plural rule, in a <code>.rb<\/code> file in <code>config\/locales<\/code> that sets <code>i18n.plural.rule<\/code>. Fill it in anyway: until every form has a translation, POEditor treats the term as untranslated. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you\u2019d like a first draft to review, Automatic Translation can pre-fill the French terms with Google Translate, DeepL or Azure AI Translator, and you can use AI translation from providers such as OpenAI or Anthropic\u2019s Claude once you add your own API key.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Automatic Translation only fills the singular form of a plural term, so complete <strong>MANY<\/strong> and <strong>OTHER<\/strong> yourself if you use it, while AI translation fills every form. Whichever you use, check the date and time formats it translates, since those have to stay valid <code>strftime <\/code>codes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To try the translations in the app, click <strong>Export<\/strong> on the French language page, set <strong>FILE FORMAT<\/strong> to <strong>YAML (.yml)<\/strong> and click <strong>Export File<\/strong>. With the default settings, the export writes every term, and an untranslated one comes out as an empty string, such as <code>title: \"\"<\/code>. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Rails treats an empty string as a translation, so the page shows nothing where that text should be, and it doesn\u2019t fall back to English even in production. An empty date format is worse, because it replaces the format from rails-i18n and the date disappears.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">While French is incomplete, switch on <strong>Advanced export options<\/strong> and set <strong>EXPORT<\/strong> to <strong>Translated<\/strong>, which leaves those terms out of the file. Alternatively, choose English as the <strong>Fallback Language<\/strong> in the same <strong>Advanced Options<\/strong>, and untranslated terms come out with their English text instead of an empty string. i18n-tasks then no longer reports them as missing, though, because they have a value. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A plural term gets the English text only in its <strong>ONE<\/strong> form, and its <strong>MANY<\/strong> and <strong>OTHER<\/strong> forms stay empty, so a count of 2 or more still shows nothing until <strong>OTHER<\/strong> is translated.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"771\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/05_export_yml-1024x771.png\" alt=\"\" class=\"wp-image-8611\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/05_export_yml-1024x771.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/05_export_yml-300x226.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/05_export_yml-767x577.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/05_export_yml-1536x1156.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/05_export_yml-2048x1541.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>Exporting the French translations as YAML<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The download is named <code>TO-DO_app_Rails_French.yml<\/code>. Save it as <code>config\/locales\/fr.yml<\/code>. It has the same structure as <code>en.yml<\/code>, under a <code>fr: root<\/code>, and keeps the comments from <code>en.yml<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># config\/locales\/fr.yml\nfr:\n&nbsp; lists:\n&nbsp; &nbsp; list:\n&nbsp; &nbsp; &nbsp; tasks_left:\n&nbsp; &nbsp; &nbsp; &nbsp; one: \"%{count} t\u00e2che restante\"\n&nbsp; &nbsp; &nbsp; &nbsp; many: \"%{count} de t\u00e2ches restantes\"\n&nbsp; &nbsp; &nbsp; &nbsp; other: \"%{count} t\u00e2ches restantes\"\n&nbsp; date:\n&nbsp; &nbsp; formats:\n&nbsp; &nbsp; &nbsp; # October 9, 2026 (strftime: %B month name, %-d day, %Y year)\n&nbsp; &nbsp; &nbsp; long: \"%-d %B %Y\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The development server picks up a new or changed locale file on the next request, so reload the page and click <strong>Fran\u00e7ais<\/strong> in the navbar. In production, restart the app after you deploy the new file.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"647\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/06_lists_page_fr-1024x647.png\" alt=\"\" class=\"wp-image-8612\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/06_lists_page_fr-1024x647.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/06_lists_page_fr-300x190.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/06_lists_page_fr-767x485.png 767w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/06_lists_page_fr-1536x971.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/06_lists_page_fr-2048x1294.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\"><em>The lists page in French, with French plurals and dates<\/em><\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">On a list page, the badge on each completed task reads <code>Termin\u00e9e<\/code> while the filter tab reads <code>Termin\u00e9es<\/code>, the dates follow the French formats, and a form that fails validation shows the rails-i18n message.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"895\" height=\"1024\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/07_list_page_fr-895x1024.png\" alt=\"\" class=\"wp-image-8613\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/07_list_page_fr-895x1024.png 895w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/07_list_page_fr-262x300.png 262w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/07_list_page_fr-768x879.png 768w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/07_list_page_fr-1342x1536.png 1342w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/07_list_page_fr-1789x2048.png 1789w\" sizes=\"auto, (max-width: 895px) 100vw, 895px\" \/><figcaption class=\"wp-element-caption\"><em>A list in French, with a validation message from rails-i18n<\/em><\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping up<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When you add a string to the app, add its key to <code>en.yml<\/code> and import the file into POEditor again. <strong>Also import translations to a language<\/strong> starts switched off once the project has terms, so switch it on and choose English, or the new terms arrive without their English text. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The import adds the new terms, which show up untranslated in French, and leaves the existing translations alone, including the English text of keys you\u2019ve reworded, unless you also switch on <strong>Overwrite existing translations<\/strong>. Switch on <strong>Mark strings as fuzzy in other languages<\/strong> as well, so the French translations of reworded keys are flagged for review. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you\u2019ve renamed or removed keys, POEditor follows the import with an <strong>Obsolete Terms<\/strong> page listing the terms that are no longer in the file, which you can tick and remove with <strong>Delete Selected<\/strong>. To automate this for every release, use the <a href=\"\/docs\/api\">POEditor API<\/a>, or a <a href=\"\/kb\/localization-file-management-with-github-bitbucket-and-gitlab-integrations\">code hosting integration<\/a> with its webhooks, to upload en.yml and download the translated files.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"\/register\/\">Create a free POEditor account<\/a> and import your own en.yml to start translating your Rails app.<\/p>\n\n\n<div class=\"call-action my-4 d-flex justify-content-between align-items-md-center gap-4 flex-column flex-lg-row\"><div><h3 class=\"fs-4\">Ready to power up localization?<\/h3><span class=\"fs-6\">Subscribe to the POEditor platform today!<\/span><\/div><a class=\"btn btn-b-primary d-flex align-items-center justify-content-center px-4 py-3 flex-shrink-0\" \n\t\t\t\t\thref=\"https:\/\/poeditor.com\/pricing\/?utm_source=blog&#038;utm_medium=btn&#038;utm_campaign=cta_pricing\">See pricing<\/a><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">For other stacks in this series, see these guides:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/blog\/python-gettext-localization\/\">Python gettext Localization: How to Translate a Python App<\/a><\/li>\n\n\n\n<li><a href=\"\/blog\/django-i18n\/\">Django i18n: How to Translate and Localize Your App<\/a><\/li>\n\n\n\n<li><a href=\"\/blog\/cakephp-localization\/\">CakePHP Localization: How to Translate a CakePHP App with i18n<\/a><\/li>\n\n\n\n<li><a href=\"\/blog\/node-js-localization\/\">Node.js Localization: How to Translate an Express App with i18n-node<\/a><\/li>\n\n\n\n<li><a href=\"\/blog\/asp-net-core-localization\/\">ASP.NET Core Localization: How to Translate a Razor Pages App with .resx Files<\/a><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently asked questions<\/h2>\n\n\n\n<div style=\"height:15px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Why does a page show \u201cTranslation missing\u201d instead of the French text?<\/summary>\n<p class=\"wp-block-paragraph\">In development, where rails new doesn\u2019t turn on fallbacks, a key that isn\u2019t in fr.yml appears on the page as a title-cased version of its last part, such as \u201cTitle\u201d or \u201cNo Account\u201d, inside a span whose title attribute names the full key, for example translation missing: fr.lists.index.title. A flash message set in a controller reads \u201cTranslation missing. Options considered were:\u201d followed by the keys it tried, starting with one such as fr.lists.create.created. First, check whether that key exists in fr.yml. Lazy lookup builds the key from the place where t is called, so moving a string from a view into a partial, or from one controller action to another, changes the key it reads. In production, config.i18n.fallbacks = true shows the English text instead, which hides the gap, so check new pages in French in development before you release them.<\/p>\n<\/details>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>How do I find keys that are missing from fr.yml?<\/summary>\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/github.com\/glebm\/i18n-tasks\" rel=\"nofollow\">i18n-tasks<\/a> gem compares your locale files with each other and with your code. Add gem &#8220;i18n-tasks&#8221;, group: :development to the Gemfile, run bundle install, and bundle exec i18n-tasks missing lists the keys that are in en.yml but not in fr.yml, as well as keys your code uses that neither file has. bundle exec i18n-tasks unused lists keys it can\u2019t find in the code, but that list also includes keys Rails reads on its own, such as activerecord.attributes and the date and time formats, and some keys whose names are built at runtime, so check each one before you delete it. A key whose French value is an empty string doesn\u2019t count as missing, and that\u2019s exactly what an export with <strong>EXPORT<\/strong> set to <strong>All<\/strong> writes for an untranslated term when no <strong>Fallback Language<\/strong> is set.<\/p>\n<\/details>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>How do I show \u201cNo tasks left\u201d instead of \u201c0 tasks left\u201d?<\/summary>\n<p class=\"wp-block-paragraph\">Give the zero case its own key and choose between the two in the view, for example list.open_count.zero? ? t(&#8220;.no_tasks_left&#8221;) : t(&#8220;.tasks_left&#8221;, count: list.open_count). The i18n gem also reads a zero entry inside a plural key for a count of 0, but its source marks that use as deprecated and points to an explicit &#8220;0&#8221; entry instead. Neither works well with POEditor. It drops a zero entry when you import en.yml, because it keeps only the plural forms a language has and neither English nor French has a zero form, and a &#8220;0&#8221; entry makes it import 0, one and other as three separate terms instead of one plural term.<\/p>\n<\/details>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Can the app choose French for visitors whose browser is set to French?<\/summary>\n<p class=\"wp-block-paragraph\">Browsers send their users\u2019 preferred languages in the Accept-Language header. The Rails guides read it with request.env[&#8220;HTTP_ACCEPT_LANGUAGE&#8221;] and point to the accept_language gem for headers that list several languages with quality values. Since the TO-DO app takes the language from the URL, the header can only help where the URL doesn\u2019t decide, which here means the unprefixed English URLs, and in practice a visitor arriving at \/. If you redirect those visitors to \/fr, do it only once, for example by setting a cookie on their first visit, because on the home page the language switcher\u2019s English link points to \/ as well and would otherwise send them back to French. A flag in the session isn\u2019t enough here, since the TO-DO app resets the session when someone logs in.<\/p>\n<\/details>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>How do I translate text stored in the database?<\/summary>\n<p class=\"wp-block-paragraph\">Rails I18n and POEditor handle the text in your code and locale files. Text that people type into the app, such as list and task names, is stored in the database and needs its own translations per record. A gem such as <a href=\"https:\/\/github.com\/shioyama\/mobility\" rel=\"nofollow\">Mobility<\/a> stores a translation for each language behind a model attribute and returns the one for the current I18n.locale, so list.name would read the French name on a French page. In the TO-DO app each user writes their own lists, so their names stay as they were typed.<\/p>\n<\/details>\n","protected":false},"excerpt":{"rendered":"<p>Ruby on Rails is a web framework for Ruby that follows the principle of convention over configuration. A new application comes with its database layer, routing, views and asset handling already set up, so there\u2019s less to configure before you can start building. As soon as an app serves people in more than one country, [&hellip;]<\/p>\n","protected":false},"author":6,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-8597","post","type-post","status-publish","format-standard","hentry","category-tutorials"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Rails i18n: How to translate a Ruby on Rails app with YAML locale files - POEditor Blog<\/title>\n<meta name=\"description\" content=\"Learn how to implement Rails i18n and localization in your Ruby on Rails app, manage translations, and simplify your workflow with POEditor.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/poeditor.com\/blog\/rails-i18n\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Rails i18n: How to translate a Ruby on Rails app with YAML locale files - POEditor Blog\" \/>\n<meta property=\"og:description\" content=\"Learn how to implement Rails i18n and localization in your Ruby on Rails app, manage translations, and simplify your workflow with POEditor.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/poeditor.com\/blog\/rails-i18n\/\" \/>\n<meta property=\"og:site_name\" content=\"POEditor Blog\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/POEditor\" \/>\n<meta property=\"article:published_time\" content=\"2026-11-02T06:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/rails-i18n.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1600\" \/>\n\t<meta property=\"og:image:height\" content=\"1145\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"POEditor\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@poeditor\" \/>\n<meta name=\"twitter:site\" content=\"@poeditor\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"POEditor\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"17 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/rails-i18n\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/rails-i18n\\\/\"},\"author\":{\"name\":\"POEditor\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#\\\/schema\\\/person\\\/db77eb2d09539eaac83dcd8f2af06b17\"},\"headline\":\"Rails i18n: How to translate a Ruby on Rails app with YAML locale files\",\"datePublished\":\"2026-11-02T06:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/rails-i18n\\\/\"},\"wordCount\":3487,\"publisher\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/rails-i18n\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/rails-i18n-1024x733.png\",\"articleSection\":[\"Tutorials\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/rails-i18n\\\/\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/rails-i18n\\\/\",\"name\":\"Rails i18n: How to translate a Ruby on Rails app with YAML locale files - POEditor Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/rails-i18n\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/rails-i18n\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/rails-i18n-1024x733.png\",\"datePublished\":\"2026-11-02T06:00:00+00:00\",\"description\":\"Learn how to implement Rails i18n and localization in your Ruby on Rails app, manage translations, and simplify your workflow with POEditor.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/rails-i18n\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/poeditor.com\\\/blog\\\/rails-i18n\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/rails-i18n\\\/#primaryimage\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/rails-i18n.png\",\"contentUrl\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/rails-i18n.png\",\"width\":1600,\"height\":1145,\"caption\":\"rails i18n\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/rails-i18n\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Rails i18n: How to translate a Ruby on Rails app with YAML locale files\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/\",\"name\":\"POEditor Blog\",\"description\":\"All about translation and localization management\",\"publisher\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#organization\",\"name\":\"POEditor\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/logo_head_512_transparent.png\",\"contentUrl\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/logo_head_512_transparent.png\",\"width\":512,\"height\":512,\"caption\":\"POEditor\"},\"image\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.facebook.com\\\/POEditor\",\"https:\\\/\\\/x.com\\\/poeditor\",\"https:\\\/\\\/www.linkedin.com\\\/company\\\/poeditor\\\/\",\"https:\\\/\\\/www.youtube.com\\\/channel\\\/UCXAk1u8N49VRMAqNneENCFA\"]},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#\\\/schema\\\/person\\\/db77eb2d09539eaac83dcd8f2af06b17\",\"name\":\"POEditor\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/95bca2c465fe6134f210cb2f2fbed4f71bf37833fb285624320e808c865695c1?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/95bca2c465fe6134f210cb2f2fbed4f71bf37833fb285624320e808c865695c1?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/95bca2c465fe6134f210cb2f2fbed4f71bf37833fb285624320e808c865695c1?s=96&d=mm&r=g\",\"caption\":\"POEditor\"},\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/author\\\/poeditor\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Rails i18n: How to translate a Ruby on Rails app with YAML locale files - POEditor Blog","description":"Learn how to implement Rails i18n and localization in your Ruby on Rails app, manage translations, and simplify your workflow with POEditor.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/poeditor.com\/blog\/rails-i18n\/","og_locale":"en_US","og_type":"article","og_title":"Rails i18n: How to translate a Ruby on Rails app with YAML locale files - POEditor Blog","og_description":"Learn how to implement Rails i18n and localization in your Ruby on Rails app, manage translations, and simplify your workflow with POEditor.","og_url":"https:\/\/poeditor.com\/blog\/rails-i18n\/","og_site_name":"POEditor Blog","article_publisher":"https:\/\/www.facebook.com\/POEditor","article_published_time":"2026-11-02T06:00:00+00:00","og_image":[{"width":1600,"height":1145,"url":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/rails-i18n.png","type":"image\/png"}],"author":"POEditor","twitter_card":"summary_large_image","twitter_creator":"@poeditor","twitter_site":"@poeditor","twitter_misc":{"Written by":"POEditor","Est. reading time":"17 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/poeditor.com\/blog\/rails-i18n\/#article","isPartOf":{"@id":"https:\/\/poeditor.com\/blog\/rails-i18n\/"},"author":{"name":"POEditor","@id":"https:\/\/poeditor.com\/blog\/#\/schema\/person\/db77eb2d09539eaac83dcd8f2af06b17"},"headline":"Rails i18n: How to translate a Ruby on Rails app with YAML locale files","datePublished":"2026-11-02T06:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/poeditor.com\/blog\/rails-i18n\/"},"wordCount":3487,"publisher":{"@id":"https:\/\/poeditor.com\/blog\/#organization"},"image":{"@id":"https:\/\/poeditor.com\/blog\/rails-i18n\/#primaryimage"},"thumbnailUrl":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/rails-i18n-1024x733.png","articleSection":["Tutorials"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/poeditor.com\/blog\/rails-i18n\/","url":"https:\/\/poeditor.com\/blog\/rails-i18n\/","name":"Rails i18n: How to translate a Ruby on Rails app with YAML locale files - POEditor Blog","isPartOf":{"@id":"https:\/\/poeditor.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/poeditor.com\/blog\/rails-i18n\/#primaryimage"},"image":{"@id":"https:\/\/poeditor.com\/blog\/rails-i18n\/#primaryimage"},"thumbnailUrl":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/rails-i18n-1024x733.png","datePublished":"2026-11-02T06:00:00+00:00","description":"Learn how to implement Rails i18n and localization in your Ruby on Rails app, manage translations, and simplify your workflow with POEditor.","breadcrumb":{"@id":"https:\/\/poeditor.com\/blog\/rails-i18n\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/poeditor.com\/blog\/rails-i18n\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/poeditor.com\/blog\/rails-i18n\/#primaryimage","url":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/rails-i18n.png","contentUrl":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2026\/10\/rails-i18n.png","width":1600,"height":1145,"caption":"rails i18n"},{"@type":"BreadcrumbList","@id":"https:\/\/poeditor.com\/blog\/rails-i18n\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/poeditor.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Rails i18n: How to translate a Ruby on Rails app with YAML locale files"}]},{"@type":"WebSite","@id":"https:\/\/poeditor.com\/blog\/#website","url":"https:\/\/poeditor.com\/blog\/","name":"POEditor Blog","description":"All about translation and localization management","publisher":{"@id":"https:\/\/poeditor.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/poeditor.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/poeditor.com\/blog\/#organization","name":"POEditor","url":"https:\/\/poeditor.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/poeditor.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2019\/11\/logo_head_512_transparent.png","contentUrl":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2019\/11\/logo_head_512_transparent.png","width":512,"height":512,"caption":"POEditor"},"image":{"@id":"https:\/\/poeditor.com\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/POEditor","https:\/\/x.com\/poeditor","https:\/\/www.linkedin.com\/company\/poeditor\/","https:\/\/www.youtube.com\/channel\/UCXAk1u8N49VRMAqNneENCFA"]},{"@type":"Person","@id":"https:\/\/poeditor.com\/blog\/#\/schema\/person\/db77eb2d09539eaac83dcd8f2af06b17","name":"POEditor","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/95bca2c465fe6134f210cb2f2fbed4f71bf37833fb285624320e808c865695c1?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/95bca2c465fe6134f210cb2f2fbed4f71bf37833fb285624320e808c865695c1?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/95bca2c465fe6134f210cb2f2fbed4f71bf37833fb285624320e808c865695c1?s=96&d=mm&r=g","caption":"POEditor"},"url":"https:\/\/poeditor.com\/blog\/author\/poeditor\/"}]}},"_links":{"self":[{"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/posts\/8597","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/comments?post=8597"}],"version-history":[{"count":5,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/posts\/8597\/revisions"}],"predecessor-version":[{"id":8620,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/posts\/8597\/revisions\/8620"}],"wp:attachment":[{"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/media?parent=8597"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/categories?post=8597"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/tags?post=8597"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}