GithubHelp home page GithubHelp logo

react-i18nliner's Introduction

react-i18nliner

react-i18nliner brings I18nliner to React via the html translate attribute. I18n doesn't get any easier than this.

TL;DR

react-i18nliner lets you do this:

<p translate="yes">
  Hey {this.props.user.name}!
  Although I am <Link to="route">linking to something</Link> and
  have some <strong>bold text</strong>, the translators will see
  <strong><em>absolutely no markup</em></strong> and will only have a
  single string to translate :o
</p>

Write your components as you normally would, and just put a translate="yes" attribute on any element/component that needs to be localized. Seriously.

And because the default translation is inline, it will be used as a fallback if a translation is missing or hasn't happened yet.

Best of all, you don't need to maintain separate translation files anymore; I18nliner will do it for you.

How does it work?

react-i18nliner preprocesses your JSX, transforming it into something truly localizable. It infers placeholders for expressions and wrappers for elements/components, and separates the localizable string. At runtime, it localizes the string, interpolating the wrappers and placeholders into the correct locations.

Localizable strings are detected both from the text nodes, as well as from translatable attributes within the translate="yes" element.

react-i18nliner enhances I18nliner, so that it can extract any of these translate="yes" strings from your codebase (in addition to regular I18n.t calls). Once you get everything translated, just stick it on I18n.translations and everything will Just Work™.

Examples

Placeholders

A placeholder will be created for the input:

<label translate="yes">
  Create <input /> new accounts
</label>

As well as for arbitrary JSX expressions:

<div translate="yes">
  Welcome back, {user.name}.
</div>

By default, placeholder keys will be inferred from the content, so a translator would see "Create %{input} keys" and "Welcome back, %{user_name}". For complicated expressions, these placeholder keys can get a bit long/gnarly. Having to retranslate strings that "changed" just because you refactored some code is terrible, so you can use keys to be a bit more explicit:

<label translate="yes">
  Create <input key="numAccounts" onChange={this.addAccounts} /> new
  accounts
</label>

In this case the extracted string would just be "Create %{num_accounts} new accounts"

Wrappers

Translators won't see any components or markup; they will be replaced with a simple wrapper notation. In this example, the extracted string would be "That is *not* the right answer":

<div translate="yes">
  That is <b>not</b> the right answer
</div>

Attributes

In addition to the "Edit your settings *here*" string, the "Your Account" will also be preprocessed, since it is a valid translatable attribute within a translated element.

<div translate="yes">
  Edit your settings <a href="/foo" title="Your Account">here</a>
</div>

Installation

1. get i18n-js and i18nliner

Get i18n-js and i18nliner installed per these instructions.

2. add react-i18nliner

npm install react-i18nliner --save

And make sure your .i18nrc file has:

{
  "plugins": [
    "react-i18nliner"
  ]
}

This will ensure that when you export strings for translation, all of your new translate="yes" stuff will get picked up.

3. preprocess all your js files with react-i18nliner

How you hook up the preprocessor will depend on how you bundle your assets:

webpack

Add this loader to your config, e.g.

{
  module: {
    loaders: [
      { test: /\.js$/, loader: "react-i18nliner/webpack-loader" }
      ...
    ],
  },
  ...
}

Check out this example app to see how everything is wired together.

browserify

Use this transform, e.g.

$ browserify -t react-i18nliner/browserify-transform app.js > bundle.js

something else?

It's not too hard to roll your own; as you can see in the loader and transform above, the heavy lifting is done by preprocess. So whether you use ember-cli, sprockets, grunt concat, etc., it's relatively painless to add a little glue code that runs preprocess on each source file.

4. add the react-i18nliner runtime extensions to i18n-js

Assuming you have a cjs-style app, do something like this:

var I18n = require("./path/to/cjs'd/i18n");
require("i18nliner/dist/lib/extensions/i18n_js")(I18n);
require("react-i18nliner/dist/extensions/i18n_js")(I18n);

If you're using AMD/<script>/something else, see the i18nliner-js README for hints; these extensions can be set up exactly the same way as i18nliner-js's.

Working with translations

Since react-i18nliner is just an i18nliner plugin, you can use the i18nliner bin / grunt task to extract translations from your codebase; it will pick up normal I18n.t usage, as well as your new translate="yes" components.

Once you've gotten all your translations back from the translators, simply stick them the giant blob 'o json on I18n.translations; it expects the translations to be of the format:

I18n.translations = {
  "en": {
    "some_key": "Hello World",
    "another_key": "What's up?"
  }
  "es": {
    "some_key": "Hola mundo",
    "another_key": "¿Qué tal?"
  },
  ...
}

Configuration / Advanced Settings

If you have certain tags that you always want to translate (e.g. <h1>), you can specify them in your .i18nrc via autoTranslateTags, e.g.

{
  "autoTranslateTags": ["h1", "h2", "h3", "h4", "h5", "h6", "p"]
}

These tags will have an implicit translate="yes", keeping your markup simple.

Note that this works for both regular HTML elements, as well as for your own custom components. For example, if you decided you wanted to use a <T> component everywhere instead of translate="yes", you could add it to autoTranslateTags, and its runtime implementation could be as simple as:

const T = React.createClass({
  render() {
    return <span {...this.props} />;
  }
})

Similarly, if you have certain tags you don't want to auto-translate (e.g. <code>), you can specify those in a similar manner:

{
  "neverTranslateTags": ["code"],
}

Then if those are ever nested in a larger translatable element, they will be assumed to be untranslatable, and a placeholder will be created for them.

Gotchas

What about pluralization? Or gender?

i18nliner does support basic pluralization (via i18n-js), but you need to use pure js for that, e.g.

<div>
  {I18n.t({one: "You have 1 item", other: "You have %{count} items"}, {count: theCount})}
</div>

i18n-js doesn't support gender-based localizations, but I do plan on making i18nliner work with other backends soon (e.g. i18next, FormatJS). Watch this space, or better yet, create a pull request ;)

Every JSX expression makes a placeholder

This kind of gets to a general rule of i18n: don't concatenate strings. For example,

return (<b translate="yes">
         You are {this.props.isAuthorized ? "authorized" : "NOT authorized"}
        </b>);

The extracted string will be "You are %{opaque_placeholder}" and the translators won't get a chance to translate the two inner strings (much less without context). So don't do that; whenever you have logically different sentences/phrases, internationalize them separately, e.g.

return (this.props.isAuthorized ?
         <b translate="yes">You are authorized</b> :
         <b translate="yes">You are NOT authorized</b>);

NOTE: in a subsequent release of react-i18nliner, the former example will cause an i18nliner:check failure. You've been warned :)

Cloning this project under Windows

This project's eslint settings force a check on the use of linefeed characters that will fail when the project is cloned with the git core.autocrlf setting set to true, which is the default on Windows. So make sure to change that setting beforehand. The easiest way to do this is probably to git init a new repo for this project and change the setting, and only then add this repo as a remote and pull from it.

Related Projects

License

Copyright (c) 2015 Jon Jensen, released under the MIT license

react-i18nliner's People

Contributors

amireh avatar celestz avatar claydiffrient avatar download avatar jenseng avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

react-i18nliner's Issues

dummy placeholders

right now react-i18nliner doesn't play nicely with i18nliner-js' JsProcessor or the i18n-js runtime, because it generates I18n.t calls with placeholders that have no corresponding values. strictly speaking there are values, but not in the I18n.t arguments, they are props of the <I18n.ComponentInterpolator>

a quick fix that would obviate the need for changing i18nliner-js or i18n-js would be to simply have dummy placeholder values that put the placeholder right back, e.g. user="%{user}". long term though it'd be great if i18nliner-js/i18n-js supported it (e.g. translateWithoutInterpolation, or change lookup to support a string-instead-of-key and make it extractable)

preserve whitespace when preprocessing

to facilitate debugging, we should preserve whitespace (or at least newlines) when converting a translate="yes" component to a <ComponentInterpolator> ... that way line numbers will match up with the original source. as it stands right now, recast is injecting some newlines, presumably for readability.

we handle this kinda rudimentarily in i18nliner (erb)

merge wrappers into contained placeholder

Spawned from #3

If all that a wrapper contains is a placeholder (no text content), we should not create a wrapper and should instead merge its component(s) into the placeholder value, e.g. consider:

<p translate="yes">Hello <b>{user}</b></p>

Ideally that should yield the following (note the lack of wrapper):

"Hello %{user}"

The value for %{user} would be <b>{user}</b>

Sample Setup

Hello, I'm trying to follow the readme but I can't seem to make it work, is there any project sample(folder) that can be followed?

auto-translate elements

react-i18nliner lets you stick translate="yes" on anything, which is cool. But what would be really cool is if you could basically say "auto translate these tags/components unless they have a translate="no". In my mind, that's pretty much the holy grail of i18n ... you get to a point where you (almost) never have to do anything or even think about i18n; your app is just automagically localized.

For starters this could be an opt-in thing, by way of .i18nrc ... basically have a setting of the elements/components that should be auto-translated. Some sensible ones to turn on would be <p />, <th />, <label />, <h1 />, etc. You probably would not want to do it for <div /> or <span />

This is also nice because it makes for an easy upgrade path from canvas_react_i18n ... just create a <Text /> component that just renders its content, and set it to auto-translate.

It is a bit of a can of worms though, i.e. it dramatically increases the likelihood of nested translatable elements. In some cases you probably only want to translate the outer one (e.g. (<p>see the <a href="/">index</a></p>). We should probably make that happen automatically, if there are no intermediate non-translatable elements.

So that gets to a second point, you might need a list of auto-don't-translates ... Consider: <label>I live in <select placeholder="state"><option>Alabama<option>Alaska... ... it would be nice to set <label /> and <option /> to auto-translate, and <select /> to never translate. Then you'd get a string for the label text (plus placeholder), and a string for each state.

piggy-back on key for placeholder name

This could provide a mechanism to specify a friendlier placeholder, rather than letting it be inferred. For example:

<p translate="yes">
  Create <input onChange={this.createAccounts} /> new user accounts
</p>

Currently this yields "Create %{input_on_change_this_create_accounts} new accounts", which is terrible, and likely to change.

If we used the key, you could do this:

<p translate="yes">
  Create <input key="numAccounts" onChange={this.createAccounts} /> new user accounts
</p>

then the string could just be "Create %{num_accounts} new accounts"

ComponentInterpolator should accept null/undefined/etc placeholder props

if a placeholder value is missing, you get <ComponentInterpolator> expected '${token}' placeholder value, none found ... right now it just does a falsy check, which is way too broad. instead we should see if it's present in props. if it is present, we should not raise an error, even if the value is undefined/null/false/0/""

key collisions in ComponentInterpolator

given:

<div translate="yes">
  Your Score
  <b className="learnerScore">{score}</b><span className="superscript">%</span>
</div>

you get a placeholder for <b> and a wrapper for the <span>, and they both get key="1" ... keys should be unique across interpolated wrapper and placeholder components

extra whitespace around placeholders/wrappers

given:

<div translate="yes">
  Your Score
  <b className="learnerScore">{score}</b><span className="superscript">%</span>
</div>

the pre-processed representation is:

<div>
  <I18n.ComponentInterpolator
    string={I18n.t("Your Score %{score} *%*", { "score": "%{score}" })}
    wrappers={{ "*": <span className="superscript">$1</span> }}
    score={<b className="learnerScore">{score}</b>}
  >$1</I18n.ComponentInterpolator>
</div>

note the whitespace between the score and the % ... we shouldn't introduce any whitespace not in the source. for adjacent placeholders this isn't a big deal, but it could be an issue for parsing wrappers (especially when dealing with nesting/etc.) ... might need to use different delimiters :'(

better inferred placeholder keys

right now it does a couple things when inferring a placeholder key from an expression:

  • snake case
  • remove invalid chars
  • remove this., this.props., and this.state. prefixes
  • remove closing tags

in addition it would be great to also:

  • remove all jsx so we are only inferring from vanilla javascript
  • remove this., this.props., and this.state. anywhere, not just in prefixes

Force a Language

Anyway to force a language for localization?
I assume by default, browser language is picked up.

webpack loader breaking on newer webpack versions

Hi John! 👋

I ran into an error cannot read property 'noParse' of undefined after upgrading to webpack 3. It seems that this.options.module is now undefined 😢 . Looking over the docs on webpack loaders (https://webpack.js.org/contribute/writing-a-loader/) it seems they discourage accessing the options directly, and they even go so far as to say that options is read-only.

I was able to get things working by explicitly adding the noParse rule to my config:

noParse: /i18nliner\/dist\/lib\/i18nliner/,

and creating a simpler version of your webpack loader that doesn't set the noParse config anymore:

const I18nliner = require('i18nliner').default;
const hasTranslatableText = require('react-i18nliner/hasTranslatableText')(I18nliner.config);
const preprocess = require('react-i18nliner/preprocess');

module.exports = function i18nloader(source) {
  this.cacheable();

  if (hasTranslatableText(source)) {
    return preprocess(source, I18nliner.config);
  }
  return source;
};

I'm not sure if there's still a way to automatically set the noParse config from within the loader.

speed up webpack and browserify

the webpack loader and browserify transform will do a full preprocess of every file they are given ... that means an esprima/acorn parse, a recast visit, and a recast print. we should skip files if we know we don't need to process them

we already have logic that does this for i18nliner:check ... a simple regex check for translatable content (based on .i18nrc settings)

ES6 support

Hi,
Seems react-i18nliner is not supporting ES6 classes?

ERROR in ./src/client.js
Module parse failed: /Users/erik/project1/node_modules/babel-
loader/index.js!/Users/erik/project1/node_modules/react-i18nliner/webpack-
loader.js!/Users/erik/project1/src/client.js Line 5: Unexpected reserved word
You may need an appropriate loader to handle this file type.

| import React from 'react';

auto-translate translatable attributes in `translate="yes"` elements

Per the spec ... if a translatable attribute has literal text, and its element (or ancestor) has translate="yes", we should preprocess that into an I18n.t call. If the element (or ancestor) has translate="no", we should not. Non-translatable attributes should never be preprocessed into I18n.t calls.

Example:

<p translate="yes">
  Update your preferences <a href="/prefs" title="User Preferences">here</a>.
</p>

This would result in the strings "Update your preferences *here*" and "User Preferences" being extracted/translated, but of course "/prefs" would not.

Fails to compile for `{" "}`

Using {" "} is a very common way to add a white-space, however when used with react-i18nliner it causes a failure to compile.

merge leading/trailing standalone elements into wrappers

Another thing for nicer translation strings... Consider:

<button onClick={this.close}>
  <Icon className="i-close" />
  <span translate="yes">Close</span>
</button>

The span is superfluous, but if we put translate="yes" on the <button /> then translators will see a placeholder for the <Icon />, which is not helpful.

If a wrapper has leading or trailing elements with no text content, we should absorb them into the wrapper. Then you can safely do:

<button onClick={this.close} translate="yes">
  <Icon className="i-close" />
  Close
</button>

an the extracted string will just be:

"Close"

i18nliner-handlebars and canvas_react_i18n both do this, we should too.

coalesce wrappers without intermediate text nodes

This doesn't really affect functionality, but it does make for nicer translation strings... Consider:

<p translate="yes">Hello <b><i>{user}</i></b></p>

Currently that yields the translation string:

"Hello ** *%{user}* **"

The double wrappers add no value for translators, and are more likely to get screwed up. We should coalesce them so that the translators see:

"Hello *%{user}*"

i18nliner-handlebars and canvas_react_i18n both do this, we should too.

Even better would be no wrappers, if all that is contained is a placeholder, but we can save that for another ticket 😄

Running tests fails on Windows

c:\ws\react-i18nliner> npm test

> [email protected] test c:\ws\react-i18nliner
> eslint . && node_modules/.bin/jest

'node_modules' is not recognized as an internal or external command,
operable program or batch file.
npm ERR! Test failed.  See above for more details.

I'll prepare a PR to fix this.

React.createClass is deprecated

I'm getting this warning when I run my tests:

Warning: Accessing createClass via the main React package is deprecated, and will be removed in React v16.0. Use a plain JavaScript class instead. If you're not yet ready to migrate, create-react-class v15.* is available on npm as a temporary, drop-in replacement. For more info see https://fb.me/react-create-class

It's being used in the definition of ComponentInterpolator. It looks from a cursory investigation like ComponentInterpolator could become a pure functional component if we were to pass keyCounter along when we recursively call interpolateAllComponents.

react-tools deprecated

Hi, doing npm install for the project I'm getting this warning:

(master) % cd react-i18nliner                                                                                                              ~/towbook
(master) % npm install                                                                                            ~/towbook/react-i18nliner
npm WARN package.json [email protected] No repository field.
npm WARN engine [email protected]: wanted: {"node":"0.8.x || 0.10.x"} (current: {"node":"0.12.4","npm":"2.10.1"})
npm WARN deprecated [email protected]: react-tools is deprecated. For more information, visit https://fb.me/react-tools-deprecated
npm WARN prefer global [email protected] should be installed with -g

some files mangled during preprocessing

this may be a recast printing bug, or it may be due to #1, or both ... i've got a file w/ some jsx like so

      <div>
        <LayoutItemHeader type="survey">
          ...
          <Block name="layout--item-header__actions">
            <SurveyHeaderActions
              survey={this.props.survey}
              permissions={this.props.permissions}
              media={this.state.media}
              handleClickPreview={this.handleClickPreview}
              handleDuplicate={this.handleDuplicate} />
          </Block>
        </LayoutItemHeader>

        <div className="survey-distributions large-content-area centered padding-trl-m-desktop padding-none-mobile">
          <div key="subtitle" className="clearfix padding-tb-l padding-xs-mobile grey-bg-mobile">
            <h2 className="margin-b-s left">
              {I18n.t("Distributions")}
            </h2>
            <DistributeBox groupSearch={this.props.groupSearch} surveyId={this.props.survey.id}>
              <button type="button" className="add-btn">
                <Text>+ <span className="add-btn__text">Distribute</span></Text>
              </button>
            </DistributeBox>
          </div>

          <DistributionList {...distributionProps} />
        </div>
      </div>

it gets preprocessed into this:

      <div>
        <LayoutItemHeader type="survey">
          ...
          <Block name="layout--item-header__actions">
            <SurveyHeaderActions
              survey={this.props.survey}
              permissions={this.props.permissions}
              me dia={t<I18n.ComponentInterpolator
                  string={I18n.t("+ *Distribute*")}
                  wrappers={{
                    "*": s.state.media}$1
                  }}>$1</I18n.ComponentInterpolator>
              handleClickPreview={this.handleClickPreview}
              handleDuplicate={this.handleDuplicate} />
          </Block>
        </LayoutItemHeader>

        <div className="survey-distributions large-content-area centered padding-trl-m-desktop padding-none-mobile">
          <div key="subtitle" className="clearfix padding-tb-l padding-xs-mobile grey-bg-mobile">
            <h2 className="margin-b-s left">
              {I18n.t("Distributions")}
            </h2>
            <DistributeBox groupSearch={this.props.groupSearch} surveyId={this.props.survey.id}>
              <button type="button" className="add-btn">
                <Text>+ <span className="add-btn__text">Distribute</span></Text>
              </button>
            </DistributeBox>
          </div>

          <DistributionList {...distributionProps} />
        </div>
      </div>

<Text> is an autoTranslateTag, and it does get preprocessed ... sort of, but in the wrong place. note the mangled media attribute inside <SurveyHeaderActions> ... totally broken

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.