@sleepyg11/localizer v0.1.5
Node.js Localizer
Simple localization module with Intl.PluralRules support for pluralization.
⚠ Work in Progress
This module is WIP. Breaking changes may appear recently. Full JSDoc and tests will be provided later.
Install
npm install @sleepyg11/localizer --saveTable of content
Basic usage
// CommonJS
const { default: Localizer } = require('@sleepyg11/localizer')
// ESM
import Localizer from '@sleepyg11/localizer'
const localizer = new Localizer({
    intl: true,
    localization: {
        'en-US': {
            'hello': 'Hello world!',
            'cats': {
                'one': '%s cat',
                'other': '%s cats',
            },
        },
        'ru-RU': {
            'hello': 'Привет мир!',
            'cats': {
                'one': '%s кот',
                'few': '%s кота',
                'other': '%s котов',
            },
        },
    },
})
// Assign localize functions to variables is fine
const l = localizer.localize
const ln = localizer.pluralize
// Get localization data by locale and key.
localize('en-US', 'hello');    // -> Hello world!
localize('ru-RU', 'hello');    // -> Привет мир!
// Using plural rules
pluralize('en-US', 'cats', 1)  // -> 1 cat
pluralize('en-US', 'cats', 2)  // -> 2 cats
pluralize('ru-RU', 'cats', 1)  // -> 1 кот
pluralize('ru-RU', 'cats', 2)  // -> 2 кота
pluralize('ru-RU', 'cats', 5)  // -> 5 котов
// Create scope with pre-defined locale
const scope = localizer.scope('ru-RU')
scope.localize('hello')        // -> Привет мир!
scope.pluralize('cats', 2)     // -> 2 котаMain structures
Localization table
JSON object with key-value localization data for all locales. For each key, value can be:
- a string
 - nested data
 - object with specific keys usable for pluralization:
- plural categories defined in Unicode CLDR
zero, one, two, few, many, other - math intervals as part of ISO 31-11
- leading 
!invert interval - have priority over plural categories
 
[3] Match number 3 only [3,5] Match all numbers between 3 and 5 (both inclusive) (3,5) Match all numbers between 3 and 5 (both exclusive) [3,5) Match all numbers between 3 (inclusive) and 5 (exclusive) [3,] Match all numbers bigger or equal 3 [,5) Match all numbers less than 5 ![3] Match all numbers except 3 ![3,5] Match all numbers less than 3 or bigger than 5 - leading 
 
 - plural categories defined in Unicode CLDR
 undefinedornullare ignored- any other type will be converted to string
 
Example
{
    // Locale
    'en-US': {
        // String
        hello: 'Hello world!',
        // Nested data
        food: {
            apple: 'Red Apple',
        },
        // Specific keys
        cats: {
            '[21]': 'Twenty one cats',
            '(50,]': 'More than 50 cats',
            'one': '%s cat',
            'other': '%s cats',
        },
    },
}Fallbacks table
Object with fallback locales for all locales.
When localization data for initial locale not found, next that match pattern in table (in order they present) will be used. Repeats until data will be found. 
* can be used as wildcard in patterns.
Example
{
  // For all English locales, fallback to `en-UK`
  'en-*': 'en-UK',
  // If `en-UK` localization missing, use `en-US` instead
  'en-UK': 'en-US',
}Plural Rules table
Object with plural rules functions for locales.
Each function take 2 arguments:
count(number) - number to pluralizeordinal(boolean) - use ordinal form (1st, 2nd, 3rd, etc.) instead of cardinal (1, 2, 3, etc.)
As result it should return one of plural category:
zero, one, two, few, many, otherExample
{
    'en-US': (count, ordinal = false) => {
        if (ordinal) return count === 2 ? 'two' : count === 1 ? 'one' : 'other'
        return count === 1 ? 'one' : 'other'
    },
    // Using `Intl.PluralRules` for custom locales.
    // Not recommended.
    'russian': (count, ordinal = false) => {
        return new Intl.PluralRules('ru-RU', { 
            type: ordinal ? 'ordinal' : 'cardinal',
        }).select(count)
    }
}Localizer class
The main class that provide localization methods.
const localizer = new Localizer()Constructor options (all optional):
safe: Should localize methods returns null instead of throw error when invalid arguments passed (default:false).intl: Use Intl.PluralRules to resolve plural categories (default:false).localization: Localization Table.fallbacks: Fallbacks Table.pluralRules: Plural Rules Table. Has priority over Intl.PluralRules.defaultLocale: Locale to use when data for all fallback locales not found (default:null).cacheLocalization: Should cache localization data for all used keys (default:true).cachePluralRules: Should cache plural rules functions for all used locales (default:true).cacheFallbacks: Should cache fallback locales for all used keys (default:true).cachePrintf: Should use cache for patterns insertion (default:true).
All cache options increase localization speed for frequently used locales, keys and data. Each individual can be disabled, which may reduce memory usage by cost of speed.
Using constructor without options equals to:
const localizer = new Localizer({
    safe: false,
    intl: false,
    localization: {},
    fallbacks: {},
    pluralRules: {},
    defaultLocale: null,
    cacheLocalization: true,
    cachePluralRules: true,
    cacheFallbacks: true,
    cachePrintf: true,
})All options can be updated in any time:
localizer.intl = true
localizer.cacheFallbacks = false
localizer.localization = {
    'en-US': {},
}LocalizerScope class
Can be taken from Localizer.scope(locale) method.
const scope = localizer.scope('ru-RU')Scope have similar localize() and pluralize() methods, except they automatically use locale defined in constructor.
All other options (like caching, fallbacks, etc.) inherit from Localizer class where it was created.
Methods
localize()
Main method for data localization.
localizer.localize(locale: string, key: string, ...args)
scope.localize(key: string, ...args)
// or
localizer.localize(options, ...args)
scope.localize(options, ...args)Data search process:
- Using 
options.rawor resolve data bylocaleandkey; - If data is string, it will be used;
 - If data is object, 
data.otherwill be used instead; - If data is 
nullorundefined, or key not found, it's ignored; - Process repeats for all fallback locales until some data will be found;
 - If nothing found, 
options.fallbackor initialkeyreturned instead. 
Has alias: localizer.l().
const localizer = new Localizer({
    localization: {
        'en-US': {
            'items': {
                'apple': 'Red Apple',
            },
            'cats': {
                'one': '%s cat',
                'other': '%s cats',
            },
        },
    },
})
// Can be assigned to variable, will work fine.
const localize = localizer.localize
// Basic usage:
localize('en-US', 'items.apple') // -> 'Red Apple'
localize('en-US', 'cats') // -> '%s cats'
// Options argument:
localize({
    locale: 'en-US',
    key: 'items.apple',
    // Can override constructor options for this call only.
    safe: true,
    cacheLocalization: false,
}) // -> Red Apple
// Raw data:
localize({
    raw: {
        one: 'One Pineapple',
        other: 'A lot of Pineapples',
    },
    fallback: 'Pineapple', // *Required* when raw is object.
}) // -> One Pineapplepluralize()
Pluralize data by using count argument and plural rules.
localizer.pluralize(locale: string, key: string, count: number, ...args)
scope.pluralize(key: string, count: number, ...args)
// or
localizer.pluralize(options, ...args)
scope.pluralize(options, ...args)Search process:
- Using 
options.rawor resolve data bylocaleandkey; - If data is string, it will be used;
 - If data is object, then:
- First interval match will be used;
 - Or, plural rules will be applied to determine plural category to use:
- Function from Plural Rules Table;
 - Or Intl.PluralRules if 
localizer.intl = true; - Or, 
data.otherform will be used; 
 
 - If data has other type, it will be used as string;
 - If data is 
nullorundefined, or key not found, it's ignored; - Process repeats for all fallback locales until some data will be found;
 - If nothing found, 
options.fallbackor initialkeyreturned instead. 
Has aliases: localizer.p() and localizer.ln().
const localizer = new Localizer({
    localization: {
        'en-US': {
            'cats': {
                '[3,5]': 'From 3 to 5 cats',
                'one': '%s cat',
                'other': '%s cats',
            },
        },
    },
})
// Can be assigned to variable, will work fine.
const pluralize = localizer.pluralize
// Basic usage:
pluralize('en-US', 'cats', 1) // -> '1 cat in my home'
pluralize('en-US', 'cats', 2) // -> '2 cats'
pluralize('en-US', 'cats', 3) // -> 'From 3 to 5 cats'
// Options argument:
pluralize({
    locale: 'en-US',
    key: 'cats',
    count: 2,
    ordinal: true, // Use ordinal form instead of cardinal
}) // -> 2 cats
// Raw data:
pluralize({
    raw: {
        one: '%s Pineapple',
        other: '%s Pineapples',
    },
    fallback: 'Pineapple', // Required when raw is object.
    locale: 'en-US', // Locale to use for plural rules. *Optional*.
    count: 2
}) // -> 2 Pineapplescount always prepended to arguments list:
pluralize({ raw: '%s', count: 10 }) // -> 10Values insertion
Localization data supports values insertion similar to sprintf-js does, with additional formatters which can be more useful for localization purposes.
Example:
const localizer = new Localizer({
    localization: {
        'en-US': { 
            hello: 'Hello, %s!',
            cats: 'I have %s cats, and one of them called %s.',
        }
    }
})
const l = localizer.l
localize('en-US', 'hello', 'Kitty') // -> Hello, Kitty!
localize({ raw: 'Goodbye, %S!' }, 'Kitty') // -> Goodbye, KITTY!
pluralize('en-US', 'cats', 10, 'Kitty') // -> I have 10 cats, and one of them called Kitty.
pluralize({ raw: 'I have %s %s.', count: 5 }, 'apples') // -> I have 5 apples.In examples below, printf() function used instead of localize() and pluralize(); insertion works identical for all of them.
import { printf } from '@sleepyg11/localizer'Utility formatters
%t: insert value as boolean
printf('%t', 1) // -> true
printf('%t', 0) // -> false%T: insert value type (name of constructor)
printf('%T', undefined)    // -> Undefined
printf('%T', null)         // -> Null
printf('%T', true)         // -> Boolean
printf('%T', 0)            // -> Number
printf('%T', 0n)           // -> BigInt
printf('%T', 'Hello')      // -> String
printf('%T', [1, 2, 3])    // -> Array
printf('%T', { a: 1 })     // -> Object
printf('%T', new Date())   // -> Date%j: insert value as JSON.
- JSON.stringify() will be used.
 BigIntconverts to string.- If JSON contains circular, 
[Circular]string will be returned. 
printf('%j', { a: 1, b: 1n }) // -> {'a':1,'b':'1'}
let withCircular = {};
withCircular.repeat = withCircular;
printf('%j', withCircular) // -> [Circular]String formatters
All passed values will be converted to string.
%s: insert value as string
printf('%s', 'New YEAR') // -> New YEAR%S: insert value as string in upper case
printf('%S', 'New YEAR') // -> NEW YEAR%c: insert value as string in lower case
printf('%c', 'New YEAR') // -> new year%C: insert value as string in upper case (similar to %S)
printf('%C', 'New YEAR') // -> NEW YEAR%n: insert value as string, first word capitalized, others in lower case
printf('%n', 'New YEAR') // -> New year%N: insert value as string, all words capitalized
printf('%N', 'New YEAR') // -> New YearNumber formatters
All passed values will be converted to number.
undefinedtreated asNaN;nulltreated as0(zero).- For some formats, precision can be applied: 
%.<precision><format>. See examples below. 
%b: insert value as binary
printf('%b', 13)          // -> 1101%o: insert value as octal
printf('%b', 13)          // -> 15%i: insert value as integer
printf('%i', 13.5)        // -> 13
printf('%i', 2147483648)  // -> 2147483648%d: insert value as signed decimal
printf('%d', 1)           // -> 15
printf('%d', -1)          // -> -1
printf('%d', 2147483648)  // -> -2147483648%u: insert value as unsigned decimal
printf('%d', 1)           // -> 1
printf('%d', -1)          // -> 4294967295%x: insert value as hexadecimal in lower case
printf('%x', 255)         // -> ff%X: insert value as hexadecimal in upper case
printf('%X', 255)         // -> FF%e: insert value in exponential form with lower e (precision can be specified)
printf('%e', 12345)       // -> 1.2345e+4
printf('%.1e', 12345)     // -> 1.2e+4%e: insert value in exponential form with upper e (precision can be specified)
printf('%E', 12345)       // -> 1.2345E+4
printf('%.1E', 12345)     // -> 1.2E+4%f: insert value as float (precision can be specified)
printf('%f', 13.579)      // -> 13.579
printf('%.2f', 13.579)    // -> 13.57
printf('%.1f', 13)        // -> 13.0Insertion order
// Insert arguments in order they present
printf('%s, %s and %s', 'One', 'Two', 'Three') // -> 'One, Two and Three'
// Insert arguments in specific order
printf('%2$s, %3$s and %1$s', 'One', 'Two', 'Three') // -> 'Two, Three and One'
// Insert values from object
printf(
    '%(first)s, %(second)s and %(third)s',
    { first: 'fish', second: 'cat', third: 'fox' }
) // -> 'fish, cat and fox'
// Combined
printf(
    '%(first)s %s, %(second)s %s and %(third)s %s',
    'Fishcl', 'Kitty', 'Foxy',
    { first: 'fish', second: 'cat', third: 'fox' }
) // -> 'fish Fishlc, cat Kitty and fox Foxy'Pad values
All (except %j) processed values can be padded to match minimal width.
//  +{width}: add whitespaces to left
printf('%+4s', 'hi')      // -> '  hi'
printf('%+4s', 'goodbye') // -> 'goodbye'
//  -{width}: add whitespaces to right
printf('%-4s', 'hi')      // -> 'hi  '
printf('%-4s', 'goodbye') // -> 'goodbye'
//  0{width}: add zeroes to left
printf('%04s', 'hi')      // -> '00hi'
printf('%04s', 'goodbye') // -> 'goodbye'
printf('%04s', '23')      // -> '0023'
printf('%04s', '-23')     // -> '0-23', don't do any sign checks!
// ++{width}: add sign for number and whitespaces to left
printf('%++5s', 23)       // -> '  +23'
printf('%++5s', -23)      // -> '  -23'
// +-{width}: add sign for number and whitespaces to right
printf('%+-5s', 23)       // -> '+23  '
printf('%+-5s', -23)      // -> '-23  '
// +0{width}: add sign for number and zeroes to left
printf('%+05s', 23)       // -> '+0023', sign checks applied!
printf('%+05s', -23)      // -> '-0023'Combined
Some examples of complex but powerful patterns:
printf('%2$+08.2f -> %1$+08.2f', 1.2345, -54.321) // -> '-0054.32 -> +0001.23'
printf('#%+06X', 16753920) // -> '#FFA500', orange color in hex
printf(
    "I live in %(city)N with my %(animal.type)s %(animal.name)n. %(animal.pronounce.0)n %(animal.feeling)S this place.",
    {
        city: "new york",
        animal: {
            pronounce: ['she', 'her'],
            type: "cat",
            name: "kitty",
            feeling: "love",
        },
    },
) // -> "I live in New York with my cat Kitty. She LOVE this place."No value
If value not provided, pattern will be returned unchanged.
printf('%s and %s', 'fish') // -> fish and %s
printf('%(hello)s and %(goodbye)s', { hello: 'Hola' }) // -> Hola and %(goodbye)sCalculating value
If value is function, in will be called without arguments and returned value will be used in pattern.
// Most common usage example: Date. Will be displayed current date
printf('%s', () => new Date())
printf('%(date)s', { date: () => new Date() })