If you ignore localization early, you'll pay for it later. As a developer, experience has taught me that l10n is much more than translation: it shapes how your product handles languages, layouts, and regional formats.

Some past projects I’ve worked on, from e-commerce platforms to internal dashboards and customer support portals, faced localization issues that created last-minute problems.

One instance was when I built an internal dashboard for a customer support team. Everything looked perfect until someone realized the product descriptions weren’t translated. 😅 The team scrambled to send the content to translators. When the translations returned, QA flagged several issues, such as some translations being too long, others not matching the original meaning, and formatting errors breaking the UI. We had no structured workflow for translation updates. Worse, the content was hardcoded in multiple places. Fixing everything delayed the release by a week.

I learned this the hard way, but you don’t have to. In this article, I'll share the mistakes I made and show you what to do differently so you can avoid the same pitfalls.

🧱 1. Build for localization from day one 🔗

When I built this support portal, localization never crossed my mind. All text was hardcoded in components, and date formatting was done manually. It worked fine before our PM requested multilingual support.

Don’t make that mistake. Avoid hardcoding text directly into your components:


<button>Save Changes</button>
<h1>Welcome to our platform</h1>
<p>Your profile has been updated successfully.</p>

Instead, set up a localization framework from the start. Use a tool like i18next to manage text with keys in a localization file:


import { useTranslation } from 'react-i18next';
const { t } = useTranslation();

<button>{t('save_changes')}</button>
<h1>{t('welcome_message')}</h1>
<p>{t('profile_updated')}</p>

And in your en.json file, store the actual text for these keys, allowing i18next to pull the correct translations dynamically:


{
  "save_changes": "Save Changes",
  "welcome_message": "Welcome to our platform",
  "profile_updated": "Your profile has been updated successfully."
}

This setup saves you from refactoring your entire project when localization becomes necessary.

Also, avoid designing your UI for English text lengths. For example, the word "Save" in German becomes "Änderungen speichern," which can easily break your layout if your buttons have fixed widths.

Instead of this:


button {
  width: 100px;
}

Use flexible sizing:


button {
  min-width: 100px;
  max-width: fit-content;
  padding: 10px 20px;
}

This flexibility allows your UI to adapt to longer text without breaking.

📅 2. Handle pluralization and date formatting properly 🔗

Localization also affects how numbers, dates, and plural forms behave across different languages. If these aren’t handled properly, they can confuse users and add extra development work later.

I once hardcoded pluralization like this:

<p>You have {messages.length} new messages.</p>

This worked in English, where adding an "s" for plurals was enough. But when we expanded to other languages, this approach wasn’t sustainable.

For example, in Russian:

  • 1 message → "1 сообщение"
  • 2-4 messages → "2 сообщения"
  • 5+ messages → "5 сообщений"

Instead of relying on conditions for every language, use ICU MessageFormat to handle plurals correctly:

<p>{t('new_messages', { count: messages.length })}</p>

In your en.json translation file:

{
  "new_messages": "{count, plural, one {# new message} other {# new messages}}"
}

For languages like Russian, additional plural categories may be required. This approach scales better, ensuring pluralization works correctly as your app adds more languages.

article-image
Localazy helps you handle plurals conveniently.
➡️ Read more on translating plurals

Date formatting can also create problems. Different regions use different formats:

  • 🇺🇸 US: MM/DD/YYYY → 04/15/2024
  • 🇪🇺 Europe: DD/MM/YYYY → 15/04/2024
  • 🇯🇵 Japan: YYYY/MM/DD → 2024/04/15

To prevent confusion, avoid writing custom logic for date formats. Instead, use  Intl.DateTimeFormat, which adapts automatically based on the user's locale:

const formattedDate = new Intl.DateTimeFormat(navigator.language).format(new Date());

For custom formats, you can pass options:

const formattedDate = new Intl.DateTimeFormat('fr-FR', { dateStyle: 'long' }).format(new Date());

This method keeps your dates consistent, localized, and intuitive without extra logic.

↔️ 3. Plan for right-to-left (RTL) support from the start 🔗

I expected a smooth process when I added Arabic and Hebrew support to the project, but the entire layout fell apart, and I ran into a few considerable localization challenges. Arrows intended to point forward suddenly faced backward, text meant to be left-aligned hugged the right edge, and the navigation menu (initially aligned neatly on the left) awkwardly shifted to the wrong side.

To avoid this, use a CSS framework that supports RTL out of the box. Tailwind CSS is the most popular option right now and comes with built-in RTL and LTR modifiers that work directly with its utility classes. For example, rtl:mr-4applies margin-right only in RTL layouts. You can also use logical spacing utilities like ms-* (margin-start) and me-*(margin-end), which automatically adapt based on direction.

These adjustments prevent your UI from breaking and eliminate the need for duplicated assets or manual layout fixes. Plan for RTL support early, and your design will stay consistent when you expand to languages like Arabic or Hebrew.

🤖 4. Automate translations with a TMS 🔗

I started by managing translations in JSON files, manually exporting spreadsheets for translators, and pasting everything back into the codebase. This worked initially, but it became unsustainable as we added more languages.

Integrate a translation management system (TMS) if you can (I adopted Localazy). With its command-line interface, you can automate translation uploads and downloads:

localazy upload
localazy download

This removes the need for manual updates, prevents errors, and allows translators to work directly in the TMS interface with full UI context.

I found this CLI starter article useful to understand translation management on Localazy. The documentation also offers a clear guide on setting up the tool, configuring translation paths, and automating the process.

📕Related read: How to automate the entire software localization process from development to translation with Localazy

🗝️ 5. Enforce key reuse and prevent translation duplication 🔗

I had a habit of adding new keys without checking if similar ones already existed. Over time, duplicate translation keys started piling up. Translators had to translate the same phrase multiple times, creating unnecessary work.

The UI also became inconsistent, with different variations of the same text appearing throughout the app. Translation files became cluttered and harder to maintain.

At first, I tried to fix this by writing a JavaScript function to scan for duplicate translation values before adding new keys. Since I was already using i18next, I wrote this script to compare keys dynamically:

import i18next from 'i18next';

function checkForDuplicateKeys() {
  const translations = i18next.store.data.en.translation; 
  const seenKeys = new Map();
  const duplicates = [];

  Object.entries(translations).forEach(([key, value]) => {
    if (seenKeys.has(value)) {
      duplicates.push(`${key} (same as ${seenKeys.get(value)})`);
    } else {
      seenKeys.set(value, key);
    }
  });

  return duplicates.length > 0 
    ? `Duplicate keys found:\n${duplicates.join('\n')}` 
    : "No duplicates found";
}

console.log(checkForDuplicateKeys());

This helped me identify duplicate translations, but it was just a shallow fix. It only checked translations on the client side, not across the entire project. And more importantly, not all duplicates are wrong. English is a simple language, and short phrases like "Open", "Save," or "Done" might show up in multiple places. In some languages, those same words translate differently depending on the context. So, trying to force everything into a single key can actually hurt clarity instead of helping.

Instead of relying on manual scripts, use a TMS that tracks duplicates across your projects. It'll help you reuse keys where it makes sense and spot the ones that need to be removed. Even better, it will do this while preserving context, so you don’t merge phrases that shouldn’t be shared by mistake.

Without proper key reuse, you'll face issues like:

  • Inconsistent UI phrasing.
  • Extra translation costs.
  • Bloated translation files that are harder to maintain.
article-image
The Localazy Duplicity Linking feature in action.

🥊 Don’t build a product that fights you back 🔗

Leave localization for later, and you’ll feel it everywhere: scattered strings, last-minute translation scrambles, layouts that fall apart in other languages. You'll end up translating the same phrase five times, going back and forth with translators and proofreaders, and trying to figure out how to fix base issues that demand a lot of developer time.

Localazy keeps all of that in check. It takes care of updates behind the scenes, keeps your keys tidy, and plugs right into your workflow without slowing you down. With features like Duplicity Linking, Translation Memory, and built-in plural support, it makes sure all's consistent across your app. And since it connects directly to your codebase, you can push and pull translations without leaving your dev environment.

Set it up early and you won’t need to rebuild later. Your product will stay clean, your team will work faster, and your users will get the experience they expect, wherever they are.