Gettext - i18n for big web project

Oleksii Vasyliev, Railsware

Gettext - i18n for big web project

Brought to you by Alexey Vasiliev, Railsware

Oleksii Vasyliev

I18N and L10N

Programming problems

  • Naming things
  • Cache invalidation
  • Time and Timezones
  • Internationalization and Localization
not happy

Internationalization and localization

adapting computer software to different languages, regional peculiarities and technical requirements of a target locale

i18n
welcome to hell

Why i18n and l10n So Dang Hard?

Our Project - codename "Didi"

Rails i18n API

en:
  errors:
    format: "%{attribute} %{message}"
    messages:
      model_invalid: "Validation failed: %{errors}"

Rails i18n API not fit

one more thing...

God of War
God of War
God of War Memes
God of War Russian

Context

Context example

Let's translate "Press":

Russian example: "Мама мыла раму"

Latin

Spiritus quidem promptus est, caro autem infirma

Literal translation

Alcohol is good, but rotten meat (Russian: "Спирт хорош, а мясо протухло")

Correct translation

The spirit is vigorous, but the flesh is weak (Russian: "Дух бодр, плоть же немощна")

Rails i18n API - context

en:
  about:
    link_text: "Press here"

Rails i18n API - context

en:
  about_page:
    link_text_for_getting_info_about_privacy_policy_details:
      "Press here"

Rails i18n API - web services

Meet GetText

GetText libraries

#!/bin/bash

echo -n 'Extracting strings from Rails app...'
bundle exec rake gettext:find > /dev/null 2>&1
echo ' Done!'

echo -n 'Adding missing SPA locale entrypoint files...'
yarn gulp locale:spa > /dev/null 2>&1
echo ' Done!'

echo -n 'Extracting strings from SPA...'
yarn gulp build:pot > /dev/null 2>&1
make spa > /dev/null 2>&1
echo ' Done!'
namespace :gettext do
  def files_to_translate
    Dir.glob("{app,lib,config,locale}/**/*.{rb,erb,haml,rabl}")
  end
end
gulp.task('build:pot', function() {
  return gulp.src('webpack/**/*.js*')
    .pipe(reactGettextParser({
      output: 'spa.pot',
      funcArgumentsMap: {
        __: ['msgid'],
        n__: ['msgid', 'msgid_plural'],
        p__: ['msgctxt', 'msgid'],
        np__: ['msgctxt', 'msgid', 'msgid_plural']
      }
    }))
    .pipe(gulp.dest('locale'));
});
// webpack
externals: {
__i18n: 'i18n'
}

// gulp
const template = _template(`
import Jed from 'jed';
import messages from '../../locale/<%= lang %>/spa.po';
const i18n = new Jed(messages);
window.i18n = i18n;
`.trim().concat('\n')
);
// code
import i18n from '__i18n';

export const __ = function() {
  return i18n.gettext.apply(i18n, arguments);
};
export const n__ = function() {
  return i18n.ngettext.apply(i18n, arguments);
};
export const p__ = function() {
  return i18n.pgettext.apply(i18n, arguments);
};
export const np__ = function() {
  return i18n.npgettext.apply(i18n, arguments);
};
import locale from 'react-native-locale-detector';
import {en, fr, ru} from 'locale';

let messages;

if (/^ru/.test(locale)) {
  messages = ru;
} else if (/^fr/.test(locale)) {
  messages = fr;
} else {
  messages = en;
}
_('N/A')

N_('Car', '%{n} Cars', 2) % { n: count } == '2 Autos'
slug = obj.new_record? ? _('Create %{resource}') : _('Update %{resource}')
slug % {resource: resource_name}

P_('Context', 'not-found') == 'not-found'
S_('Context|not-found') == 'not-found'

PN_('Fruit', 'Apple', 'Apples', 3) == 'Äpfel'
PN_('Fruit', 'Apple', 'Apples', 1) == 'Apfel'
        
%html{lang: FastGettext.locale}

day.localize(FastGettext.locale).to_additional_s('Ed')

def date_long_label(date)
  date.to_date.localize(FastGettext.locale).to_additional_s('MMMMd')
end

def date_short_label(date)
  date.to_date.localize(FastGettext.locale).to_additional_s('MMMd')
end
        
// react
<ActionButton
  label={__('Deselect')}
  onClick={onDeselectClick} />

{
  isLoading &&
  <Loader
    text={__('Please wait data are loading…')}/>
}
    
i18n.translate("There is one translation.")
  .onDomain("messages")
  .withContext("male")
  .ifPlural(num, "There are %d translations.")
  .fetch(num);

moment.locale(this.getLocale());

moment(date).format(dateFormat.toString());
moment(currentDate).subtract(offset, 'days').format('YYYY-MM-DD')

You don't (may not) need Moment.js

Name Size(gzip) Tree-shaking Pattern Timezone Support Locale
Moment.js 329K(69.6K) No OO Good(moment-timezone) 123
Luxon 59.9K(17.2K) No OO Good(Intl) -
date-fns 78.4k(13.4k) Yes Functional Not yet 50
dayjs 6.5k(2.6k) without plugins No OO Not yet 130
gettext plural
gettext plural

Web... it is separate story...

web

Cultural peculiarities

Alcohol consumption is prohibited in Islam

web dermomed

GetText editors

Weblate
Weblate
Weblate
Weblate

What about SPA (translations on frontent) + API backend?

GetText Pros

GetText Cons

<Thank You!> Questions?

Contact information

QuestionsSlide