npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@gravity-ui/webpack-i18n-assets-plugin

v1.0.0

Published

Localize your Webpack bundle with multiplication and inlining i18n texts

Downloads

3

Readme

🌍 webpack-i18n-assets-plugin

A plugin for Webpack that replaces calls to localization functions (i18n) with target texts.

Features

  • Inlines i18n texts into the bundle (while substituting parameters into the final string)
  • Generates assets for all locales in one build
  • The plugin works only for production builds!
  • Supports only literals as keys in the localization function argument (template strings and variables are not allowed)

📝 How to use

  1. Install the package:

    npm i -D @gravity-ui/webpack-i18n-assets-plugin
  2. Connect the plugin to Webpack (example for @gravity-ui/app-builder):

    Example for webpack config (webpack.config.js):

    const {I18nAssetsPlugin} = require('@gravity-ui/webpack-i18n-assets-plugin');
    
    // For example. Read all files with localized texts and store in this mapping
    const locales = {
        en: {},
        ru: {},
        tr: {},
    };
    
    module.exports = {
        output: {
            filename: '[name].[locale].js', // [locale] is required in filename
        },
    
        plugins: [
            new I18nAssetsPlugin({
                locales
            })
        ]
    }

    Example if you want create assets manifests for each locale (webpack.config.js):

    const {applyPluginToWebpackConfig} = require('@gravity-ui/webpack-i18n-assets-plugin');
    
    const locales = {
        en: {},
        ru: {},
        tr: {},
    };
    
    // Some exist webpack config
    const webpackConfig = {
        plugins: [ ... ],
        ...
    };
    
    // When using applyPluginToWebpackConfig, the WebpackAssetsManifest plugin will also be connected,
    // which will generate assets manifests for each locale.
    module.exports = applyPluginToWebpackConfig(webpackConfig, {locales});

    Example if you use @gravity-ui/app-builder:

    import type {ServiceConfig} from '@gravity-ui/app-builder';
    import {applyPluginToWebpackConfig, Options} from '@gravity-ui/webpack-i18n-assets-plugin';
    
    const locales = {
        en: {},
        ru: {},
        tr: {},
    };
    
    // When using applyPluginToWebpackConfig, the WebpackAssetsManifest plugin will also be connected,
    // which will generate assets manifests for each locale.
    const config: ServiceConfig = {
        client: {
            webpack: (originalConfig) => applyPluginToWebpackConfig(originalConfig, {locales}),
        },
    }
  3. Configure dynamic statics from the asset manifest on the server (example with @gravity-ui/app-layout):

    import {createRenderFunction, createLayoutPlugin} from '@gravity-ui/app-layout';
    
    const renderLayout = createRenderFunction([
        createLayoutPlugin({
            manifest: ({lang = 'en'}) => {
                return `assets-manifest.${lang}.json`;
            },
            publicPath: '/build/',
        }),
    ]);
    
    app.get((req, res) => {
        res.send(
            renderLayout({
                title: 'Home page',
                pluginsOptions: {
                    layout: {
                        name: 'home',
                    },
                },
            }),
        );
    });

🔧 Settings

By default, the plugin is configured to work with the @gravity-ui/i18n library, but you can customize the processing for any other i18n library.

importResolver

Type: ImportResolver

The function that processes imports and marks which of the imports should be considered as localization functions (subsequently, calls to the marked identifiers are processed by the replacer).

The signature is similar to the original importSpecifier from webpack.

Example:

const importResolver = (source: string, exportName: string, _identifierName: string, module: string) => {
    // If you need to ignore processing modules based on specific paths, you can handle such a case this way.
    if (module.startsWith('src/units/compute')) {
        return undefined;
    }

    // Processing the default import of a global function
    // import i18n from 'ui/utils/i18n'
    if (source === 'ui/utils/i18n' && exportName === 'default') {
        return {
            resolved: true,
            keyset: undefined,
        };
    }

    // Processing the import of a helper function and specifying that it belongs to the common keyset (namespace).
    // import {ci18n} from 'ui/utils/i18n'
    if (source === 'ui/utils/i18n' && exportName === 'ci18n') {
        return {
            resolved: true,
            keyset: 'common',
        };
    }

    return undefined;
};

declarationResolver

Type: DeclarationResolver

The function that processes variable declarations and marks which variables should be considered as localization functions (subsequently, calls to the marked identifiers are processed by the replacer function).

Example:

import type {VariableDeclarator} from 'estree';

const declarationResolver = (declarator: VariableDeclarator, module: string) => {
    // If you need to ignore processing modules based on specific paths, you can handle such a case this way.
    if (module.startsWith('src/units/compute')) {
        return undefined;
    }

    // Processing function declarations like const i18nK = i18n.bind(null, 'keyset');
    if (
        declarator.id.type === 'Identifier' &&
        declarator.id.name.startsWith('i18n') &&
        declarator.init &&
        isI18nBind(declarator.init)
    ) {
        return {
            functionName: declarator.id.name,
            keyset: getKeysetFromBind(declarator.init),
        };
    }

    return undefined;
};

replacer

Type: Replacer

A function that processes localization function calls and returns a replacement as a string.

Example:

import type {VariableDeclarator} from 'estree';
import type {ReplacerArgs, ReplacerContext} from '@gravity-ui/webpack-i18n-assets-plugin';

function replacer(
    this: ReplacerContext,
    {callNode, key: parsedKey, keyset: parsedKeyset, localeName}: ReplacerArgs,
) => {
    let key = parsedKey;
    let keyset = parsedKeyset;
    let params: Expression | SpreadElement | undefined;

    const getStringValue = (node: Expression | SpreadElement) => {
        if (node.type === 'Literal' && typeof node.value === 'string') {
            return node.value;
        }

        throw new Error('Incorrect argument type in localizer call');
    };

    // Processing a call with one argument i18nK('key')
    if (callNode.arguments.length === 1) {
        key = getStringValue(callNode.arguments[0]);
    } else if (callNode.arguments.length === 2) {
        // Processing i18n('keyset', 'key') or i18nK('key', {params})
        const [firstArg, secondArg] = callNode.arguments;

        // Call i18n('keyset', 'key')
        if (secondArg.type === 'Literal') {
            keyset = getStringValue(firstArg);
            key = getStringValue(secondArg);
        } else {
            // Call i18nK('key', {params})
            key = getStringValue(firstArg);
            params = secondArg;
        }
    } else if (callNode.arguments.length === 3) {
        // Call i18n(namespace, key, params)
        const [firstArg, secondArg, thirdArg] = callNode.arguments;
        keyset = getStringValue(firstArg);
        key = getStringValue(secondArg);
        params = thirdArg;
    } else {
        throw new Error('Incorrect count of arguments in localizer call');
    }

    // Be sure to process the key obtained from the function call argument.
    // If the function is related to a keyset, after modifying the code, the keyset can be inserted into the key (this is a plugin feature).
    // If you use the key from ReplacerArgs, it comes without the keyset and does not need to be processed.
    const keyParts = key.split('::');
    if (keyParts.length === 2) {
        key = keyParts[1];
    }

    const value = this.resolveKey(key, keyset);

    // Implement replacement options based on your needs here.
    // For example, if the key is plural, return a function call, etc.

    return JSON.stringify(value);
};

collectUnusedKeys

Type: [Boolean] (default - false)

Enables the mode for collecting unused keys in the project. After building, it creates a file named unused-keys.json.

To ensure proper functionality, it is always necessary to return a detailed format in the Replacer function. This is important because during replacement, there is a possibility of modifying automatically determined keys and keysets.

Frameworks settings

Gravity i18n

Functions for handling localization function calls from the library @gravity-ui/i18n.

The ready to use functions are located here.

An example of the code that functions will work with:

// The importResolver only considers the default import at the path ui/utils/i18n.
import i18n from 'ui/utils/i18n';

// The declarationResolver handles variables whose value is a call to i18n.bind.
const i18nK = i18n.bind(null, 'component.navigation');

// The replacer handles calls to identifiers found by the importResolver and declarationResolver
// This means the following calls will be processed:
i18nK('some_key');
i18nK('some_plural_key', { count: 123 });
i18nK('some_key_with_param', { someParam: 'hello' });
i18n('component.navigation', 'some_key');
i18n('component.navigation', 'some_plural_key', { count: 123 });
i18n('component.navigation', 'some_key_with_param', { someParam: 'hello' });

The Replacer additionally performs the following:

  1. Inline the parameters into a string. For example, if the key value is as follows:

    const keyset = {
        some_key: 'string value with {{param}}'
    };
    
    i18nK('some_key', {param: getSomeParam()})
    // After the replacements, we will get:
    `string value with ${getSomeParam()}`
  2. Substitutes a self-invoking function for plural keys:

    const keyset = {
        pural_key: [
            'one_form {{count}}',
            'few_form {{count}}',
            'many_form {{count}}',
            'other_form {{count}}',
        ],
    };
    
    i18nK('pural_key', {count: getSomeCount()})
    
    // After the replacements, we will get:
    (function(f,c){
        const v=f[!c ? "zero" : new Intl.PluralRules("${locale}").select(c)];
        return v && v.replaceAll("{{count}}",c);
    })({
        "one": "one_form {{count}}",
        "few": "few_form {{count}}",
        "many": "many_form {{count}}",
        "other": "other_form {{count}}"
    }, getSomeCount())

ℹ️ FAQ

How does this compare to webpack-localize-assets-plugin?

To implement this plugin, an idea from the webpack-localize-assets-plugins package was used (for which many thanks to the package creator!).

The differences are as follows:

  • A more convenient API that allows you to work with any kind of internationalization functions (including namespaces-helpers like useTranslation from i18next, imported functions from other modules, etc.)
  • Correct generation of source maps relative to the source code
  • There is only support for webpack 5. Webpack 4 support has been removed.