project/
├── public/
│ └── locales/
│ ├── uk/
│ │ ├── common.json
│ │ ├── calculator.json
│ │ └── errors.json
│ ├── en/
│ │ ├── common.json
│ │ ├── calculator.json
│ │ └── errors.json
│ ├── ru/
│ │ └── ...
│ └── de/
│ └── ...
2
Налаштування i18next (Vanilla JS)
Створіть src/i18n.js:
import i18next from 'i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18next
.use(HttpBackend) // Завантаження файлів з /public/locales
.use(LanguageDetector) // Автовизначення мови браузера
.init({
fallbackLng: 'en', // Резервна мова
debug: true,
// Підтримувані мови
supportedLngs: ['uk', 'en', 'ru', 'de', 'fr'],
// Налаштування бекенду
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
// Налаштування детектора
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
caches: ['localStorage', 'cookie'],
},
// Інтерполяція
interpolation: {
escapeValue: false, // React вже escaped
},
});
export default i18next;
Locale файл public/locales/uk/calculator.json:
{
"title": "Наукові калькулятори",
"subtitle": "280+ безкоштовних калькуляторів онлайн",
"quadratic": {
"title": "Квадратне рівняння",
"description": "Розв'язання квадратного рівняння виду ax² + bx + c = 0",
"coefficient_a": "Коефіцієнт a",
"coefficient_b": "Коефіцієнт b",
"coefficient_c": "Коефіцієнт c",
"calculate": "Обчислити",
"results": {
"roots": "Корені рівняння:",
"discriminant": "Дискримінант:",
"no_roots": "Рівняння не має дійсних коренів"
}
},
"errors": {
"invalid_input": "Некоректне введення",
"coefficient_zero": "Коефіцієнт a не може дорівнювати нулю",
"calculation_error": "Помилка обчислення: {{error}}"
},
"common": {
"loading": "Завантаження...",
"error": "Помилка",
"success": "Успіх",
"cancel": "Скасувати",
"save": "Зберегти"
}
}
Англійський переклад public/locales/en/calculator.json:
{
"title": "Scientific Calculators",
"subtitle": "280+ free online calculators",
"quadratic": {
"title": "Quadratic Equation",
"description": "Solve quadratic equation ax² + bx + c = 0",
"coefficient_a": "Coefficient a",
"coefficient_b": "Coefficient b",
"coefficient_c": "Coefficient c",
"calculate": "Calculate",
"results": {
"roots": "Equation roots:",
"discriminant": "Discriminant:",
"no_roots": "The equation has no real roots"
}
},
"errors": {
"invalid_input": "Invalid input",
"coefficient_zero": "Coefficient a cannot be zero",
"calculation_error": "Calculation error: {{error}}"
},
"common": {
"loading": "Loading...",
"error": "Error",
"success": "Success",
"cancel": "Cancel",
"save": "Save"
}
}
3
Використання в коді
import './i18n';
import i18next from 'i18next';
// Простий переклад
document.querySelector('h1').textContent = i18next.t('title');
// Вкладені ключі
document.querySelector('.subtitle').textContent = i18next.t('quadratic.description');
// З інтерполяцією
const error = 'Division by zero';
alert(i18next.t('errors.calculation_error', { error }));
// Зміна мови
function changeLanguage(lng) {
i18next.changeLanguage(lng, () => {
// Оновити всі переклади на сторінці
updatePageTranslations();
});
}
// Language switcher
document.getElementById('lang-uk').addEventListener('click', () => changeLanguage('uk'));
document.getElementById('lang-en').addEventListener('click', () => changeLanguage('en'));
HTML з data-атрибутами:
<div class="calculator">
<h1 data-i18n="quadratic.title"></h1>
<p data-i18n="quadratic.description"></p>
<form>
<label data-i18n="quadratic.coefficient_a"></label>
<input type="number" name="a">
<button data-i18n="quadratic.calculate"></button>
</form>
<!-- Language switcher -->
<select id="language-select">
<option value="uk">Українська</option>
<option value="en">English
// Функція оновлення перекладів на сторінці
function updatePageTranslations() {
document.querySelectorAll('[data-i18n]').forEach(element => {
const key = element.getAttribute('data-i18n');
element.textContent = i18next.t(key);
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
const key = element.getAttribute('data-i18n-placeholder');
element.placeholder = i18next.t(key);
});
}
// Оновити при завантаженні
i18next.on('initialized', updatePageTranslations);
i18next.on('languageChanged', updatePageTranslations);
Частина 3: React i18next
1
Налаштування React i18next
src/i18n.js:
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
supportedLngs: ['uk', 'en', 'ru'],
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
react: {
useSuspense: true,
},
});
export default i18n;
src/App.jsx:
import React, { Suspense } from 'react';
import { useTranslation } from 'react-i18next';
import './i18n';
function Calculator() {
const { t, i18n } = useTranslation('calculator');
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
};
return (
<div>
<h1>{t('title')}</h1>
<p>{t('quadratic.description')}</p>
<select value={i18n.language} onChange={(e) => changeLanguage(e.target.value)}>
<option value="uk">Українська</option>
<option value="en">English</option>
<option value="ru">Русский</option>
</select>
<button>{t('quadratic.calculate')}</button>
</div>
);
}
function App() {
return (
<Suspense fallback="Loading...">
<Calculator />
</Suspense>
);
}
export default App;
Частина 4: Локалізація дат, чисел та валют
Intl API (вбудований у браузер)
// Дати
const date = new Date();
const ukDate = new Intl.DateTimeFormat('uk-UA').format(date);
// "07.02.2026"
const enDate = new Intl.DateTimeFormat('en-US').format(date);
// "2/7/2026"
const fullDate = new Intl.DateTimeFormat('uk-UA', {
dateStyle: 'full',
timeStyle: 'short'
}).format(date);
// "п'ятниця, 7 лютого 2026 р., 14:30"
// Числа
const number = 1234567.89;
const ukNumber = new Intl.NumberFormat('uk-UA').format(number);
// "1 234 567,89"
const enNumber = new Intl.NumberFormat('en-US').format(number);
// "1,234,567.89"
// Валюти
const price = 1234.56;
const uah = new Intl.NumberFormat('uk-UA', {
style: 'currency',
currency: 'UAH'
}).format(price);
// "1 234,56 ₴"
const usd = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(price);
// "$1,234.56"
// Відносний час
const rtf = new Intl.RelativeTimeFormat('uk', { numeric: 'auto' });
rtf.format(-1, 'day'); // "вчора"
rtf.format(2, 'day'); // "через 2 дні"
rtf.format(-3, 'month'); // "3 місяці тому"
Частина 5: RTL (Right-to-Left) підтримка
1
Визначення RTL мов
const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur'];
function isRTL(language) {
return RTL_LANGUAGES.includes(language);
}
// Встановлення direction
function setDirection(language) {
const direction = isRTL(language) ? 'rtl' : 'ltr';
document.documentElement.setAttribute('dir', direction);
document.documentElement.setAttribute('lang', language);
}
i18next.on('languageChanged', (lng) => {
setDirection(lng);
});
CSS для RTL:
/* Замість margin-left/right використовуйте logical properties */
.container {
margin-inline-start: 20px; /* margin-left для LTR, margin-right для RTL */
margin-inline-end: 10px; /* margin-right для LTR, margin-left для RTL */
padding-inline: 15px; /* padding-left та padding-right */
}
/* Або з fallback */
.card {
float: left;
}
[dir="rtl"] .card {
float: right;
}
/* Flexbox автоматично змінює напрямок */
.flex-container {
display: flex;
flex-direction: row; /* Автоматично реверсується в RTL */
}
/* Icons що потребують дзеркального відображення */
[dir="rtl"] .icon-arrow-right {
transform: scaleX(-1);
}
مثال RTL (Arabic):
هذا مثال على النص من اليمين إلى اليسار في اللغة العربية
Частина 6: Best Practices
✅ Рекомендації:
Структуруйте переклади за функціональністю , а не за сторінками
Використовуйте намespaces для великих проектів (common, calculator, errors)
Pluralization — підтримка множини:
{
"item": "{{count}} елемент",
"item_plural": "{{count}} елементи",
"item_many": "{{count}} елементів"
}
Context — різні переклади для різних контекстів:
{
"save": "Зберегти",
"save_male": "Збережений",
"save_female": "Збережена"
}
Не hardcode тексти — всі тексти через i18n
Тестуйте з різними мовами — особливо короткими/довгими словами
Locale detection order : URL → Cookie → LocalStorage → Browser
⚠️ Поширені помилки:
❌ Хардкод тексту у JS/HTML
❌ Конкатенація строк (замість інтерполяції)
❌ Абсолютні позиції (left/right) замість logical properties
❌ Ігнорування RTL testing
❌ Використання SVG з текстом (краще font icons)
Частина 5: Translation Management Platforms
Crowdin — Translation Management System
1
Налаштування Crowdin проекту
Signup на Crowdin.com
Створіть новий проект → Software Localization
Виберіть source language: Ukrainian
Додайте target languages: English, Russian, German, French
Upload source files:
# Crowdin CLI
npm install -g @crowdin/cli
# crowdin.yml
project_id: '12345'
api_token: 'YOUR_API_TOKEN'
files:
- source: /public/locales/uk/*.json
translation: /public/locales/%two_letters_code%/%original_file_name%
Upload files:
crowdin upload sources
Download перекладені файли:
crowdin download
Lokalise Alternative
Platform
Безкоштовний план
GitHub інтеграція
Machine Translation
Ціна
Crowdin
✅ Для open-source
✅ Так
✅ Google Translate, DeepL
$0-99/міс
Lokalise
⚠️ 14 днів trial
✅ Так
✅ Так
$120-600/міс
Weblate
✅ Self-hosted завжди free
✅ Так
✅ Так
€0 (self-host)
Transifex
✅ Для open-source
✅ Так
✅ Так
$199-999/міс
Частина 6: SEO для багатомовних сайтів
URL Structure для мов
Підхід
Приклад
Переваги
Недоліки
Subdirectories
site.com/uk/ site.com/en/
✅ Простота ✅ Один домен
⚠️ Довші URLs
Subdomains
uk.site.com en.site.com
✅ Незалежне хостування
❌ Розділений domain authority
ccTLDs
site.com.ua site.com
✅ Найкраще geo-targeting
❌ Дорого ❌ Складне управління
Query params
site.com?lang=uk
—
❌ НЕ рекомендовано для SEO
✅ Рекомендація: Використовуйте subdirectories (/uk/, /en/) — найкращий баланс між SEO та простотою
hreflang теги для багатомовності
Sitemap для мультимовності
https://site.com/uk/calculator
https://site.com/en/calculator
Частина 7: Testing багатомовності
Automated і18n Testing
// tests/i18n.test.js
import i18n from '../src/i18n';
describe('i18n Tests', () => {
test('All translations loaded', () => {
const languages = ['uk', 'en', 'ru'];
languages.forEach(lang => {
expect(i18n.hasResourceBundle(lang, 'common')).toBe(true);
});
});
test('No missing translations', () => {
const ukKeys = Object.keys(i18n.getResourceBundle('uk', 'common'));
const enKeys = Object.keys(i18n.getResourceBundle('en', 'common'));
expect(ukKeys.sort()).toEqual(enKeys.sort());
});
test('Pluralization works', () => {
i18n.changeLanguage('uk');
expect(i18n.t('item', { count: 1 })).toBe('1 елемент');
expect(i18n.t('item', { count: 2 })).toBe('2 елементи');
expect(i18n.t('item', { count: 5 })).toBe('5 елементів');
});
test('Interpolation works', () => {
i18n.changeLanguage('uk');
const result = i18n.t('welcome', { name: 'Олександр' });
expect(result).toBe('Вітаємо, Олександр!');
});
test('Fallback to default language', () => {
i18n.changeLanguage('unknown-lang');
// Повинен fallback до 'uk' (default)
expect(i18n.t('common:save')).not.toContain('common:save');
});
});
Visual Regression Testing для RTL
// Playwright visual test
import { test, expect } from '@playwright/test';
test('RTL Layout Screenshot Comparison', async ({ page }) => {
// LTR (українська)
await page.goto('http://localhost:3000/uk/calculator');
await expect(page).toHaveScreenshot('calculator-uk-ltr.png');
// RTL (арабська)
await page.goto('http://localhost:3000/ar/calculator');
await expect(page).toHaveScreenshot('calculator-ar-rtl.png');
// Перевірка що елементи дзеркальні
const buttonLTR = await page.locator('.submit-button').boundingBox();
await page.goto('http://localhost:3000/ar/calculator');
const buttonRTL = await page.locator('.submit-button').boundingBox();
// Button повинен бути з протилежного боку
expect(buttonLTR.x).toBeGreaterThan(buttonRTL.x);
});
Частина 8: Advanced Features
Lazy Loading translations
// i18n.js зі lazy loading
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
i18n
.use(Backend)
.init({
lng: 'uk',
fallbackLng: 'uk',
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
// Lazy load namespaces
allowMultiLoading: false
},
ns: ['common'], // Завантажити тільки common на старті
defaultNS: 'common'
});
// Динамічне завантаження namespace
async function loadCalculatorNamespace() {
await i18n.loadNamespaces('calculator');
// Тепер можна використовувати t('calculator:solve')
}
Translation Cache
// Caching translations у localStorage
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LocalStorageBackend from 'i18next-localstorage-backend';
import ChainedBackend from 'i18next-chained-backend';
i18n
.use(ChainedBackend)
.init({
backend: {
backends: [
LocalStorageBackend, // Спершу localStorage
Backend // Потім HTTP
],
backendOptions: [{
expirationTime: 7 * 24 * 60 * 60 * 1000 // 7 днів
}, {
loadPath: '/locales/{{lng}}/{{ns}}.json'
}]
}
});
Context-aware translations
// uk/common.json
{
"delete": "Видалити",
"delete_male": "Видалений",
"delete_female": "Видалена",
"delete_plural": "Видалені"
}
// Використання context
const gender = user.gender; // 'male', 'female', або null
const count = items.length;
const key = count > 1 ? 'delete_plural'
: gender === 'male' ? 'delete_male'
: gender === 'female' ? 'delete_female'
: 'delete';
const message = t(key);
Частина 9: Best Practices
✅ Translation Quality Guidelines:
Complete sentences — ніколи не розбивайте речення на частини:
// ❌ ПОГАНО
{
"greeting_start": "Вітаємо,",
"greeting_end": "в нашому додатку!"
}
// ✅ ДОБРЕ
{
"greeting": "Вітаємо, {{name}}, в нашому додатку!"
}
Context comments — додавайте коментарі для перекладачів:
{
"save_comment": "Button label for saving calculator results",
"save": "Зберегти"
}
Character limit warnings — вка зуйте якщо є обмеження:
{
"button_short_comment": "Max 10 characters for mobile button",
"button_short": "OK"
}
Variables naming — зрозумілі назви змінних:
// ❌ ПОГАНО
{
"msg": "{{a}} має {{b}} {{c}}"
}
// ✅ ДОБРЕ
{
"user_has_items": "{{userName}} має {{count}} елементів"
}
Separation of concerns — окремі файли для різних функцій:
locales/
uk/
common.json — загальні тексти (кнопки, лейбли)
calculator.json — математична термінологія
errors.json — повідомлення про помилки
validation.json — валідація форм
Частина 10: Troubleshooting
Проблема 1: Missing translations показує ключ
Симптом: Замість перекладу відображається calculator:solve
Рішення:
// Перевірте чи завантажений namespace
console.log(i18n.hasResourceBundle('uk', 'calculator')); // => false
// Завантажіть namespace
await i18n.loadNamespaces('calculator');
// Або налаштуйте default namespaces
i18n.init({
ns: ['common', 'calculator'], // Завантажити обидва
defaultNS: 'common'
});
Проблема 2: Pluralization не працює
Симптом: Завжди показує одну форму множини
Рішення:
// Правильна структура для української (3 форми)
{
"item_one": "{{count}} елемент", // 1, 21, 31...
"item_few": "{{count}} елементи", // 2-4, 22-24...
"item_many": "{{count}} елементів" // 0, 5-20, 25-30...
}
Проблема 3: RTL layout ламається
Симптом: Арабська версія виглядає однаково як LTR
Рішення:
// Динамічно встановлюйте dir attribute
useEffect(() => {
const isRTL = ['ar', 'he', 'fa', 'ur'].includes(i18n.language);
document.documentElement.dir = isRTL ? 'rtl' : 'ltr';
document.documentElement.lang = i18n.language;
}, [i18n.language]);
Висновок
Інтернаціоналізація — не luxury, а necessity для глобальних проектів:
✅ i18next — найкращий вибір для більшості проектів
✅ Intl API — вбудована локалізація дат/чисел
✅ RTL підтримка — logical properties + dir attribute
✅ Pluralization + Context — для якісних перекладів
🚀 Рекомендований workflow:
Встановіть i18next з першого дня проекту
Створіть структуру /public/locales/
Використовуйте CSS logical properties
Налаштуйте auto language detection
Додайте language switcher у UI
Тестуйте з RTL мовами