README.md in i18n-js-4.1.0 vs README.md in i18n-js-4.2.0

- old
+ new

@@ -138,95 +138,130 @@ ```yaml embed_fallback_translations: enabled: true ``` +##### `export_files`: + +By default, i18n-js will export only JSON files out of your translations. This +plugin allows exporting other file formats. To use it, add the following to your +configuration file: + +```yaml +export_files: + enabled: true + files: + - template: path/to/template.erb + output: "%{dir}/%{base_name}.ts" +``` + +You can export multiple files by define more entries. + +The output name can use the following placeholders: + +- `%{dir}`: the directory where the translation file is. +- `%{name}`: file name with extension. +- `%{base_name}`: file name without extension. +- `%{digest}`: MD5 hexdigest from the generated file. + +The template file must be a valid eRB template. You can execute arbitrary Ruby +code, so be careful. An example of how you can generate a file can be seen +below: + +```erb +/* eslint-disable */ +<%= banner %> + +import { i18n } from "config/i18n"; + +i18n.store(<%= JSON.pretty_generate(translations) %>); +``` + +This template is loading the instance from `config/i18n` and storing the +translations that have been loaded. The +`banner(comment: "// ", include_time: true)` method is built-in. The generated +file will look something like this: + +```typescript +/* eslint-disable */ +// File generated by i18n-js on 2022-12-10 15:37:00 +0000 + +import { i18n } from "config/i18n"; + +i18n.store({ + en: { + "bunny rabbit adventure": "bunny rabbit adventure", + "hello sunshine!": "hello sunshine!", + "time for bed!": "time for bed!", + }, + es: { + "bunny rabbit adventure": "conejito conejo aventura", + bye: "adios", + "time for bed!": "hora de acostarse!", + }, + pt: { + "bunny rabbit adventure": "a aventura da coelhinha", + bye: "tchau", + "time for bed!": "hora de dormir!", + }, +}); +``` + #### Plugin API You can transform the exported translations by adding plugins. A plugin must -inherit from `I18nJS::Plugin` and can have 3 class methods. The following -example shows how the built-in `embed_fallback_translations` plugin is -implemented. +inherit from `I18nJS::Plugin` and can have 4 class methods. To see a real +example, see +[lib/i18n-js/embed_fallback_translations_plugin.rb](https://github.com/fnando/i18n-js/blob/main/lib/i18n-js/embed_fallback_translations_plugin.rb) +Here's the base `I18nJS::Plugin` class with the documented api: + ```ruby # frozen_string_literal: true module I18nJS - require "i18n-js/plugin" - - class EmbedFallbackTranslationsPlugin < I18nJS::Plugin - CONFIG_KEY = :embed_fallback_translations - - # This method must set up the basic plugin configuration, like adding the - # config's root key in case your plugin accepts configuration (defined via - # the config file). + class Plugin + # This method is responsible for transforming the translations. The + # translations you'll receive may be already be filtered by other plugins + # and by the default filtering itself. If you need to access the original + # translations, use `I18nJS.translations`. # - # If you don't add this key, the linter will prevent non-default keys from - # being added to the configuration file. - def self.setup - I18nJS::Schema.root_keys << CONFIG_KEY + # Make sure you always check whether your plugin is active before + # transforming translations; otherwise, opting out transformation won't be + # possible. + def self.transform(translations:, config:) + translations end # In case your plugin accepts configuration, this is where you must validate # the configuration, making sure only valid keys and type is provided. # If the configuration contains invalid data, then you must raise an # exception using something like # `raise I18nJS::Schema::InvalidError, error_message`. def self.validate_schema(config:) - return unless config.key?(CONFIG_KEY) + end - plugin_config = config[CONFIG_KEY] - valid_keys = %i[enabled] - schema = I18nJS::Schema.new(config) - - schema.expect_required_keys(valid_keys, plugin_config) - schema.reject_extraneous_keys(valid_keys, plugin_config) - schema.expect_enabled_config(CONFIG_KEY, plugin_config[:enabled]) + # This method must set up the basic plugin configuration, like adding the + # config's root key in case your plugin accepts configuration (defined via + # the config file). + # + # If you don't add this key, the linter will prevent non-default keys from + # being added to the configuration file. + def self.setup end - # This method is responsible for transforming the translations. The - # translations you'll receive may be already be filtered by other plugins - # and by the default filtering itself. If you need to access the original - # translations, use `I18nJS.translations`. + # This method is called whenever `I18nJS.call(**kwargs)` finishes exporting + # JSON files based on your configuration. # + # You can use it to further process exported files, or generate new files + # based on the translations that have been exported. + # # Make sure you always check whether your plugin is active before - # transforming translations; otherwise, opting out transformation won't be - # possible. - def self.transform(translations:, config:) - return translations unless config.dig(CONFIG_KEY, :enabled) - - translations_glob = Glob.new(translations) - translations_glob << "*" - - mapping = translations.keys.each_with_object({}) do |locale, buffer| - buffer[locale] = Glob.new(translations[locale]).tap do |glob| - glob << "*" - end - end - - default_locale = I18n.default_locale - default_locale_glob = mapping.delete(default_locale) - default_locale_paths = default_locale_glob.paths - - mapping.each do |locale, glob| - missing_keys = default_locale_paths - glob.paths - - missing_keys.each do |key| - components = key.split(".").map(&:to_sym) - fallback_translation = translations.dig(default_locale, *components) - - next unless fallback_translation - - translations_glob.set([locale, key].join("."), fallback_translation) - end - end - - translations_glob.to_h + # processing files; otherwise, opting out won't be possible. + def self.after_export(files:, config:) end end - - I18nJS.register_plugin(EmbedFallbackTranslationsPlugin) end ``` To distribute this plugin, you need to create a gem package that matches the pattern `i18n-js/*_plugin.rb`. You can test whether your plugin will be found by @@ -240,11 +275,24 @@ To list missing and extraneous translations, you can use `i18n lint:translations`. This command will load your translations similarly to how `i18n export` does, but will output the list of keys that don't have a matching translation against the default locale. Here's an example: -![`i18n lint:translations` command in action](https://github.com/fnando/i18n-js/raw/main/images/i18njs-check.gif) +```console +$ i18n lint:translations +=> Config file: "./config/i18n.yml" +=> Require file: "./config/environment.rb" +=> Check "./config/i18n.yml" for ignored keys. +=> en: 232 translations +=> pt-BR: 5 missing, 1 extraneous, 1 ignored + - pt-BR.actors.github.metrics (missing) + - pt-BR.actors.github.metrics_hint (missing) + - pt-BR.actors.github.repo_metrics (missing) + - pt-BR.actors.github.repository (missing) + - pt-BR.actors.github.user_metrics (missing) + - pt-BR.github.repository (extraneous) +``` This command will exist with status 1 whenever there are missing translations. This way you can use it as a CI linting. You can ignore keys by adding a list to the config file: @@ -298,12 +346,12 @@ Notice that only literal strings can be used, as in `i18n.t("message")`. If you're using dynamic scoping through variables (e.g. `const scope = "message"; i18n.t(scope)`), they will be skipped. ```console -$ i18n lint:scripts --config test/config/lint.yml --require test/config/require.rb -=> Config file: "test/config/lint.yml" -=> Require file: "test/config/require.rb" +$ i18n lint:scripts +=> Config file: "./config/i18n.yml" +=> Require file: "./config/environment.rb" => Node: "/Users/fnando/.asdf/shims/node" => Available locales: [:en, :es, :pt] => Patterns: ["!(node_modules)/**/*.js", "!(node_modules)/**/*.ts", "!(node_modules)/**/*.jsx", "!(node_modules)/**/*.tsx"] => 9 translations, 11 missing, 4 ignored - test/scripts/lint/file.js:1:1: en.js.missing