synacor / preact-i18n

Simple localization for Preact.
BSD 3-Clause "New" or "Revised" License
206 stars 18 forks source link

Ability to have markup in strings #13

Closed billneff79 closed 6 years ago

billneff79 commented 7 years ago

We have use cases where we would like to insert markup inside of a phrase that should be translated as a whole unit, e.g. <a href="http://example.com>Please click here</a> to open the page

As the system currently works, we would need to construct it as, <Text id='link.pre'/><a href="http://example.com><Text id='link.text'/></a><Text id='link.post'/> because different languages might translate this sentence akin to Please, to open the page <a href="http://example.com>click here</a> or other variations of ordering, which forces a before, during, and after markup set of text. This gets out of control if you have more than one piece of markup in your text. It also means that a translation service has to try to translate this set of strings:

link.pre=''
link.text='Please click here'
link.post='to open the page'

I image that is difficult for a translation service.

I wonder if this could be solved by simply allowing for markup in the string, and having a boolean flag for something like <Text id="openPage" dangerouslySetInnerHTML />

billneff79 commented 7 years ago

After thinking about this more, I wonder if we should just do composition with preact-markup. E.g.

import Markup from 'preact-markup';
<Markup markup={<Text id="somethingWithMarkup"/>} type="html"/>
pl12133 commented 7 years ago

This appears to work with the suggestion of preact-markup, but with the assistance of withText to get a plain string to use as the markup attribute:

@withText(({ id }) => ({ markup: <Text id={id} /> })
class Notify extends Component {
  render({ markup }) {
    assert(markup === 'I am a message <b>with markup!</b>.');
    return (
      <span>
        <Markup markup={markup} type="html" trim={false} />
      <span>
    );
  }
}

Now I wonder: Should we create a helper component to do this for the user (e.g. <MarkupText>) which does this automatically, or should we document this as a recommended solution?

pl12133 commented 7 years ago

Here's a proof of concept for <MarkupText>. It's pretty small for how useful this feature could be.

@withText((props) => ({ markup: <Text {...props} /> }))
class MarkupText extends Component {
  render({ markup }) {
    return (
      <Markup markup={markup} trim={false} type="html" />
    );
  }
}
billneff79 commented 7 years ago

I like those approaches. We'll just want to make sure that any solution avoids pulling in preact-markup unnecessarily (i.e. don't use it as a result of an object value or anything)

developit commented 7 years ago

Can simplify via Localizer:

const MarkupText = ({ id, children, ...props }) => (
  <Localizer>
    <Markup
      {...props}
      markup={<Text id={id}>{children}</Text>}
      type="html"
    />
  </Localizer>
);

... or without preact-markup at all:

const MarkupText = ({ id, children, ...props }) => (
  <Localizer>
    <Html {...props} html={<Text id={id}>{children}</Text>} />
  </Localizer>
);
const Html = ({ html, ...props }) => (
  <span dangerouslySetInnerHTML={{ __html: html }} {...props} />
);
jhoffmcd commented 6 years ago

I was messing around with this, and I had a more complex use case I thought I would share.

Say I take this one step further, and I need to do template replacement into a string that required both markup and a component, in this case <Icon />

You can extend @developit <MarkupText /> component to include a fields argument like this:

const MarkupText = ({ id, fields, children, ...props }) => (
  <Localizer>
    <Html {...props} html={<Text id={id} fields={fields}>{children}</Text>} />
  </Localizer>
)

Then use preact-render-to-string to pre-render that component when passing it to <MarkupText />:

<MarkupText id='nav.text' fields={
  { navArrowLeft: `<span class='nav__icons'>${render(<Icon name='prev' />)}</span>` }
} />

Assuming that your translation string contains {{navArrowLeft}}.