I want an elegant solution for adding styling like bold, italic, or a color for only a part of a translation.
Example
Let say that I have this i18n translation:
'my-page.confim-customer.popup.warning': 'All customer files must be authorized by the customer himself, please be sure to invite the customer to scan this QR code.'
Now i want that the words "must be authorized by the customer himself" will be in bold and in yellow. (Design requirement)
What can I do?
Solution 1 - Split in i18n
I can simply split the translation to three parts in i18n file:
// i18n/en.js
'my-page.confim-customer.popup.warning.part1': 'All customer files'
'my-page.confim-customer.popup.warning.part2': 'must be authorized by the customer himself,'
'my-page.confim-customer.popup.warning.part3': 'please be sure to invite the customer to scan this QR code.'
and then apply the css i want only on the "part2".
// MyComponent.jsx
const first = i18n.t('my-page.confim-customer.popup.warning.part1');
const second = i18n.t('my-page.confim-customer.popup.warning.part2');
const thrid = i18n.t('my-page.confim-customer.popup.warning.part3');
return (
<>
<div>
{first}
<span className="bold yellow">{second}</span>
{thrid}
</div>
</>
);
Pros
It works
Cons
Polute the i18n files with lots of partial string for each lines.
When change is required it is a hassel to change the view and translation
Solution 2 - Split in code
I keep i18n in the same translation but I add seprator in it (let say $#$):
'my-page.confim-customer.popup.warning': 'All customer files $#$must be authorized by the customer himself,$#$ please be sure to invite the customer to scan this QR code.'
And then in my component i split it:
// MyComponent.jsx
const [first, second, thrid] = i18n.t('my-page.confim-customer.popup.warning').split('$#$')
return (
<>
<div>
{first}
<span className="bold yellow">{second}</span>
{thrid}
</div>
</>
);
Pros
It works
Offload the headache to javascirpt, keep the translation in one i18n key
Cons
Need to process the key before using it
When change is needed (let say we add a new seperator for a section to be italic), we need to alter the way we process the key and change the view to match. not dynamic.
Note
I'm looking for a vanilla solution, as I'm not working with React or a popular framework.
i18next has a special component Trans. There you can use any components inside your string like:
"test" : "All customer files <yelow>must be authorized by the customer himself,</yelow> please be sure to invite the customer to scan this QR code."
...
<Trans
i18nKey='test'
components={{ yelow: <span className="bold yellow"/> }}
/>
The strings split into separate keys are extremely not translator-friendly. It would be hard for translators to provide quality translations since they need to know where each part is used just not to break the UI.
In addition, the translation for some languages likely will have a different word order. The situation is even more complicated in RTL languages.
So, it's definitely recommended to avoid such approaches with string splitting.
As a Vanilla JS solution that can work in your case, I would recommend the Lingui library.
In short, it allows developers to do the following thing by using the #lingui/macro package:
import { Trans } from "#lingui/macro"
<Trans>Read the docs.</Trans>;
Then you can extract all your localizable texts to the .po or .json file by using the Lingui CLI.
Components and HTML tags are replaced with dummy indexed tags (<0></0>) which has several advantages:
both custom React components and built-in HTML tags are supported
change of component props doesn't break the translation
the message is extracted as a whole sentence (this seems to be obvious, but most i18n libs simply split the message into pieces by tags and translate them separately)
Further reading:
Internationalization of JavaScript apps
Working with LinguiJS CLI
#lingui/macro docs
Related
We have an alert component that renders important information for the user. However, this component has somewhat an abstraction where you just need to pass the content as an array of string.
const Component = () => {
const alertContent = ['This is the first things', 'Second thing', 'third thing'];
return (
<AlertComponent content={alertContent} />
)
}
This successfully displays the content as a list. However, we need to bolden parts of the text for emphasis. I thought about changing the strings in the array to include html tags, then use dangerouslySetInnerHTML to render them. Something like:
const alertContent = ['This is an <b>important</b> text', ...];
return (
<>
{alertContent.map(content => <span dangerouslySetInnerHTML={{__html: content}} />)}
</>
)
I've read about cross-site scripting attacks, but most articles I've read talk about user inputs and third party APIs. Does hard-coding the html prevent this? Or do I still need to use sanitizers?
If you, the coder, create all of the the HTML to be inserted, and are sure that it doesn't have any XSS vulnerabilities, then yes, it'll be safe.
dangerouslySetInnerHTML is named as such to tell you primarily that, if used incorrectly, it's very easy to open your app up to security problems. But if the HTML that gets set is hard-coded and safe, then dangerouslySetInnerHTML is safe too.
Sanitizers are necessary when the markup comes from user input, or from an external service. They're not needed if the markup comes entirely from your own code.
That said, in this particular situation:
However, we need to bolden parts of the text for emphasis
Why not just use JSX instead?
const alertContent = [<>This is an <b>important</b> text</>, ...];
Writing in JSX when possible is much preferable to having to resort to dangerouslySetInnerHTML.
If a text to be rendered in dangerouslySetInnerHTML is set by administrators or proved users, it should be safe and no problem.
But if normal users can set the texts or html contents, you should consider it.
You can provide html UI editors such as TinyMCE for users to set html content and implement some logic to strip the dangerous html tags such as iframe, script , etc on your backend apps.
I have to send some emails and I simply want to re-use as much code/knowledge as possible (just because), for this I want to render a React component to raw HTML with inline classes.
I have managed to render a React component to static markup via:
const TestMail = () => {
return (
<div>
<h1 className="text-xl font-bold border-b">You have a new Test Email on Productlane</h1>
<p className="border-b">Something something</p>
<a href="https://productlane.io/feedback" className="bg-purple-600">
Open
</a>
</div>
)
}
export function testMailer({ to }: IParams) {
const emailHtml = ReactDOMServer.renderToStaticMarkup(<TestMail />)
const processedHtml = juice(emailHtml, {
webResources: {
// relativeTo: "app/core/styles/index.css",
},
})
return {
async send() {
console.warn("trying to SEND")
console.warn(processedHtml)
},
}
}
This outputs the raw html string without the styles, so I figured I really need to pass the compiled css for the inliner to do its job
<div><h1 class="text-xl font-bold border-b">You have a new Test Email on Productlane</h1><p class="border-b">Something something</p>Open</div>
You can see from the snippet I'm trying to use Juice to inline the styles, however, I can seem to get the classes to be rendered in the html, any idea how to achieve this?
Right I’ve been doing some digging and this is my plan for handling emails.
Transpile down styles sheets to style attribute using Juice, abstract HTML4 tables as react components to allow full email client support.
Support {{ parameters }} leave them in your outputted HTML and pass it through Handlebars to replace them just before sending the email.
Option 1:
Use NextJS static html export to generate HTML files from said React components.
Configure build command to run custom Juice script on outputted files.
Reference the exported files using handlebars to apply the per user context e.g. { name: “David” }. I’m doing this in my sendEmail() function.
Option 2
Use NextJS server endpoint to compile the handlebars template with the per user context. See this article for reference.
You could also replace custom Juice script with this CLI tool or this npm package. Optionally you can even use Inky to abstract away HTML4 tables.
Alternatively if you only want partial email client support NextJS can inline the CSS into the head with this experimental flag discusses here. For full support you will need CSS in style attribute.
I have a lambda function sendEmail(email: string, templateName: string, context: Record<unknown, any>) which has the hubs template files bundle inside it. When the email is sent it then process the context using handlebars compile().
I'm working on a custom i18n module and would love to replace this code (this is a an "about-us" page):
const messages = (await import(`./about-us.${locale}.json`))
.default as Messages;
By
const messages = (
await import(`./${__filename.replace('.tsx', `.${locale}.json`)}`)
).default as Messages;
Unfortunately __filename resolves to /index.js (I guess because of Webpack?) - is there any way to achieve what am I trying to do in my example or this would need to be built-in Next.js directly to work?
Refactor this so consumers don't know about the filesystem
Spoiler: I'm not going to tell you how to access __filename with Next.js; I don't know anything about that.
Here's a pattern that is better than what you propose, and which evades the problem entirely.
First, setup: it sounds like you've got a folder filled with these JSON files. I imagine this:
l10n/
about-us.en-US.json
about-us.fr-FR.json
contact-us.en-US.json
contact-us.fr-FR.json
... <package>.<locale>.json
That file organization is nice, but it's a mistake to make every would-be l10n consumer know about it.
What if you change the naming scheme later? Are you going to hand-edit every file that imports localized text? Why would you treat Future-You so poorly?
If a particular locale file doesn't exist, would you prefer the app crash, or just fall back to some other language?1
It's better to create a function that takes packageName and localeCode as arguments, and returns the desired content. That function then becomes the only part of the app that has to know about filenames, fallback logic, etc.
// l10n/index.js
export default function getLang( packageName, localeCode ) {
let contentPath = `${packageName}.${localeCode}.json`
// TODO: fallback logic
return JSON.parse(FS.readFileSync(contentPath, 'utf8'))
}
It is a complex job to locate and read the desired data while also ensuring that no request ever gets an empty payload and that each text key resolves to a value. Dynamic import + sane filesystem is a good start (:applause:), but that combination is not nearly robust-enough on its own.
At a previous job, we built an entire microservice just to do this one thing. (We also built a separate service for obtaining translations, and a few private npm packages to allow webapps to request and use language packs from our CMS.) You don't have to take it that far, but it hopefully illustrates that the problem space is not tiny.
1 Fallback logic: e.g. en-UK & en-US are usually interchangeable; some clusters of Romance languages might be acceptable in an emergency (Spanish/Portuguese/Brazilian come to mind); also Germanic languages, etc. What works and doesn't depends on the content and context, but no version of fallback will fit into a dynamic import.
You can access __filename in getStaticProps and getServerSideProps if that helps?
I pass __filename to a function that needs it (which has a local API fetch in it), before returning the results to the render.
export async function getStaticProps(context) {
return {
props: {
html: await getData({ page: __filename })
}, // will be passed to the page component as props
};
}
After a long search, the solution was to write a useMessages hook that would be "injected" with the correct strings using a custom Babel plugin.
A Webpack loader didn't seem like the right option as the loader only has access to the content of the file it loads. By using Babel, we have a lot more options to inject code into the final compiled version.
in my application I will have a large infobox with a lot of text. They will often be on a numbered list. I would like to find the easiest way for them so that I can easily send them from a developer to a copywriter.
Is there any smart way to create an HTML list (ul or ol) using vue i18n without breaking it into multiple variables in message. I would like to use e.g. Component interpolation, but only call it once: <i18n path="info" tag="li">. Quick code to show what I mean:
//translate.js (fragment)
const messages = {
en: {
sample: 'Sample text with numbered list. 1) Find the problem 2) Ask on Slack 3) Solve the problem 4) Keep happy with vue'
}
}
export const i18n = new VueI18n({
locale: 'en',
messages
});
//App.vue (fragment)
<ul>
<i18n path="sample" tag="li">
</i18n>
</ul>
I would like the text in 'sample' to be divided by sub-points. I care about any way, it doesn't have to be Component interpolation.
You can use an array in your localization as shown in the manual - https://kazupon.github.io/vue-i18n/guide/messages.html
Then you can build the template like this:
<ul>
<i18n v-for="(item,index) in NumberedList" :key="index" :path="`sample[${index}]`" tag="li" />
</ul>
Problem
I'm implementing translations for my Angular 6 application. It needs to support static texts of the app in multiple languages. I don't need dynamic translations during runtime - I just need producing an app with texts in a language chosen during build.
Angular has the whole page on internationalization, but it focuses solely on situations where all the texts are hard coded in the templates, like in the code below:
<p>This is a hard coded text</p>
That's hardly a situation in my template. Additionally it seems to me impossible or extremely dirty to write the whole application with all the texts hard-coded in templates, so if that's the only mechanism available for translation, then it seems completely useless to me.
In my components I very often have texts bound to some JavaScript variables (or to be precise object properties), like in the code below:
<li *ngFor="let navigationEntry of navigationEntries">
<a [href]="navigationEntry.url">
{{ navigationEntry.text }}
</a>
</li>
Let's say the variable is still static, just being stored in the component or a service, e.g.:
navigationEntries: Array = [
{
url: "/",
text: "Home",
},
{
url: "/login",
text: "Login",
},
{
url: "/admin",
text: "Admin panel",
},
];
Question
Is there a way to use Angular native translation (internationalization) mechanisms to translate the anchor texts (or actually the text properties of the navigationEntries members) during build time? The structure of the JavaScript data and the HTML of the template can change.
If Angular native internationalization mechanisms can't handle that (but then I wonder what's their use at all), then what are other solutions that could help with implementing such translations? I found ngx-translate library, but it's providing translations which can be dynamically changed at runtime and ideally I wouldn't like to add that unnecessary overhead of dynamic solution watching on all the translated texts.
Yes, you can use Angular native translation mechanim to do this. We use the following pattern a lot to get around the missing dynamic translations in angular i18n:
You have an array of messages, and then use it with ngFor and ngSwitch to template the messages, something like this for your example:
<li *ngFor="let navigationEntry of navigationEntries">
<ng-container [ngSwitch]="navigationEntry.text">
<a [href]="navigationEntry.url" *ngSwitchCase="'Home'" i18n="##home">
Home
</a>
<a [href]="navigationEntry.url" *ngSwitchCase="'Login'" i18n="##login">
Login
</a>
<a [href]="navigationEntry.url" *ngSwitchCase="'Admin panel'" i18n="##adminPanel">
Admin panel
</a>
<ng-container>
</li>
It's somewhat verboose - but it works for a surprising amount of use cases.
Note, the i18n id (the string following the ##) is just an unique id, which is used in the xlf translation file. The string can be anything, I use the message itself, if possible, for readability and reuse.