Switch the language in React without using external libraries - javascript

What ways to change language in React can you suggest without using external libraries? My way is to use the ternary operator {language === 'en'? 'title': 'titre'}. If language is en, displaytitle if not, display titre. What other way can you recommend. For example, that the translations should be placed in a separate json file.
Code here: https://stackblitz.com/edit/react-eu9myn
class App extends Component {
constructor() {
super();
this.state = {
language: 'en'
};
}
changeLanguage = () => {
this.setState({
language: 'fr'
})
}
render() {
const {language} = this.state;
return (
<div>
<p>
{language === 'en' ? 'title' : 'titre'}
</p>
<button onClick={this.changeLanguage}>change language</button>
</div>
);
}
}

Internationalization (i18n) is a hard problem with a few existing, standard solutions designed by expert translators and linguists to account for the breadth of language quirks across the world. You shouldn't generally try to come up with your own solution, even when you are fluent in all target languages.
That doesn't mean you need a library (you could implement one of those standards yourself) but writing the i18n logic inline will not scale and probably won't work well.
The easiest case of i18n is if you're translating strings that do not depend on context and are complete sentences with no interpolation. You could get away with a very basic approach there, like using a big dictionary of translations and just looking up each string in it. It would look sort of like your ternary but at least it would scale for many languages, and it would be reasonable to do that with no library:
l10n = {
'title': {en: 'title', fr: 'titre'}
}
<p>
{l10n['title'][lang]}
</p>
However, if there is going to be string interpolation in your website/application/whatever, please consider a library that implements, say, ICU.
Now, let me show you why it would be a bad idea. Suppose you have the string "you can see (n) rocks" where you want to replace (n) with an actual number, and you want the sentence to be grammatically correct so you need to compute number agreement, right ? so, "0 rocks", "1 rock", "2+ rocks"… looks like English plural is just adding an "s" (not true, but let's assume for now), you could implement that with ternaries. I see you used French in your example so, how about that ? "0 cailloux", "1 caillou", "2+ cailloux". Right, there are multiple plural forms in French. How do you write your code to account for that ? And what if you need a German translation ? maybe the translator will decide that the object should go first in the sentence, rather than last. How does your code handle word order based on language ?
All these problems should be delegated to the translator who encodes them into an ICU string, which is then evaluated by some code given a context to get a correct translation. Whether you use a library or implement it yourself, what you want in the end is some function — let's call it localize(string, context) that is pretty much independent from React and that you use in your components like this:
import localize from './somewhere'
<p>
{localize('title')}
</p>
If you really want to, you can pass the locale as an argument and have it stored in React's state somehow. This library decided it wasn't necessary because real users rarely switch language and it's OK to reload the whole application when that happens.

I just implemented a simple language component for work that uses a Localisation context/provider and a dictionary (e.g JSON). I'll go through the steps, and there's a workable codesandbox example at the end. This is a very basic approach, but it works well for us at the moment.
The example has:
1) A simple "dictionary" that contains the tokens you want to translate in each language defined by a short code
{ EN: { welcome: 'Welcome' }, FR: { welcome: 'Bienvenue' }, IT: { welcome: 'Benvenuto' } };
2) An initial state and reducer that you can update when the language changes
export const initialState = {
defaultLanguage: 'EN',
selectedLanguage: 'IT'
}
export function reducer(state, action) {
const { type, payload } = action;
switch (type) {
case 'LANGUAGE_UPDATE': {
return { ...state, selectedLanguage: payload };
}
default: return state;
}
}
3) A Localisation Context/Provider. You can wrap your code in the provider and every child component can get access to the state through the context. We import the dictionary and state/reducer, create the new context and then set up the provider into which we pass the state and dictionary.
import dictionary from './dictionary';
import { initialState, reducer } from './localisationReducer';
export const LocalisationContext = React.createContext();
export function LocalisationProvider({ children }) {
const localisationStore = useReducer(reducer, initialState);
return (
<LocalisationContext.Provider value={{ localisationStore, dictionary }}>
{children}
</LocalisationContext.Provider>
);
}
4) An example app. You can see the LocalisationProvider wrapping the other elements, but also a dropdown, and a component called Translate. I'll describe those next.
<LocalisationProvider>
<Dropdown />
<div className="App">
<h1>
<Translate token="welcome" />
</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
</LocalisationProvider>
5) The dropdown accesses the Localisation context and builds a dropdown with the languages. The key part is the handleSelected function which uses the dispatch from the localisation store to change the state (update the language):
import { LocalisationContext } from './localisation';
const langs = [
{ shortCode: 'EN', label: 'English' },
{ shortCode: 'FR', label: 'Français' },
{ shortCode: 'IT', label: 'Italiano' }
];
export function Dropdown() {
const {
localisationStore: [ state, dispatch ]
} = useContext(LocalisationContext);
const { selectedLanguage } = state;
const handleSelected = (e) => {
const { target: { value } } = e;
dispatch({ type: 'LANGUAGE_UPDATE', payload: value });
}
function getOptions(langs, selectedLanguage) {
return langs.map(({ shortCode, label }) => {
return <option value={shortCode}>{label}</option>
});
}
return (
<select onChange={handleSelected}>
{getOptions(langs, selectedLanguage)}
</select>
);
}
6) The Translate component which also accesses the state and dictionary through the context, and performs the translation based on the selected language.
import { LocalisationContext } from './localisation';
export default function Translator({ token }) {
const {
localisationStore: [state], dictionary
} = useContext(LocalisationContext);
const {
selectedLanguage, defaultLanguage
} = state;
const translatedToken = dictionary[selectedLanguage][token] || dictionary[defaultLanguage][token];
return (
<Fragment>
{translatedToken}
</Fragment>
);
}
Here's the codesandbox example for you to explore. Just select a new language from the dropdown to see the main "welcome" text change.

Related

Using ES6 classes, including setters and methods

I'm new to programming in Svelte. I would like to be able to use a method on an ES6 class instance in order to dynamically change values being used on my SPA. (Using svelte-spa-router is not an option, unfortunately.)
To get to the crux of the problem in a simplified from:
This is router.js:
import { writable } from 'svelte/store';
export class Router {
constructor(pageMap) {
this.pageMap = pageMap;
this.current = {};
this.currentName = '';
}
get name() {
return this.currentName;
}
set name(pageName) {
this.current = this.pageMap[pageName];
this.currentName = this.current.name;
}
navigate(target) {
this.name = target;
console.log(this.currentName);
}
}
and this is App.js:
<script>
import { Router } from './router';
const pageMap = {
start: {
title: 'start',
name: 'world!',
},
end: {
title: 'end',
name: '-- it works!!!',
},
}
const page = new Router(pageMap);
page.name = 'start';
</script>
<h1>Hello {page.currentName}</h1>
<button on:click={() => page.navigate('end')}>
change to "it works"
</button>
<button on:click={() => page.navigate('start')}>
change back to "world!"
</button>
The desired behavior is that the page.currentName value changes with the button presses. The output to the console on button presses is correct: "--it works!!!", or "world!". However, the text remains "Hello world!", so the value change is not traveling outside the class instance. If I had some way of saying "this = this" upon invoking the navigate method, that would probably solve the problem...
I suspect the correct answer involves writable stores, but I haven't quite been able to figure it out.
I suspect the correct answer involves writable stores
That is correct and trying to use classes like this is not helpful, at least with how Svelte operates right now.
Stores have to be declared at the top level of a component to be usable with $ syntax, putting them inside properties of classes and hiding them behind getters or setters just gets in the way.
I would just use a function that returns an object containing the stores and API you actually need, which then can be destructured right away and used in the markup. E.g.
import { writable, derived } from 'svelte/store';
export function router(pageMap) {
const current = writable({});
const currentName = derived(current, $current => $current.name ?? '');
function navigate(target) {
current.set(pageMap[target]);
}
return {
navigate,
currentName,
};
}
<script>
import { router } from './router';
const pageMap = {
start: {
title: 'start',
name: 'world!',
},
end: {
title: 'end',
name: '-- it works!!!',
},
}
const { navigate, currentName } = router(pageMap);
navigate('start');
</script>
<h1>Hello {$currentName}</h1>
<button on:click={() => navigate('end')}>
change to "it works"
</button>
<button on:click={() => navigate('start')}>
change back to "world!"
</button>
REPL example
You can do something similar with a class, but if you destructure it, the this binding will be broken, so all functions have to be bound manually or you have to pull out the store on its own and keep accessing the functions via the instance.
REPL example

GraphQL Query to Array List using React?

I am currently working on a gatsby website and am mostly using react components doing so. The way our jobs posting page currently works is that it fetches a list of jobs from an array that stores the information for each position and maps them out with proper stylization.
positions.js (section)
const Positions = () => (
<StyledPositions>
<GlobalContainer>
{PositionList.map((position, index) => (
<StyledPosition key={index}>
<StyledPositionName>
<Link to={position.link} activeClassName="active">
{position.name}
</Link>
</StyledPositionName>
</StyledPosition>
))}
</GlobalContainer>
</StyledPositions>
);
export default Positions;
PositionList is the array and it looks like this.
positionslist.js
const Positionlist = [
{
name: "Senior Software Engineer- Infrastructure",
link: "/careers/open_positions/sr_cloud_eng",
},
{
name: "System Software Engineer",
link: "/careers/open_positions/system_soft_eng",
},
{
name: "Software Engineer (Database Development)",
link: "/careers/open_positions/soft_eng_database",
},
];
export default Positionlist;
What I'm trying to do is to populate the job posting site from a GraphQL query from an external job posting management site. I am able to fetch the information fine, but I'd like to somehow turn the information into an array like positionslist.js so that positions.js can simply map the information the same way. The query looks like this
query MyQuery {
allLever {
edges {
node {
text
categories {
commitment
location
team
}
applyUrl
}
}
}
}
All GraphQL queries store their result inside props.data as you can see in Gatsby's tutorial. At this point, you only need to enter to the nested structure like:
import * as React from 'react'
import { graphql } from 'gatsby'
const Positions = ({data}) => {
let PositionList = data.edges;
return <StyledPositions>
<GlobalContainer>
{PositionList.map(({node: position}) => (
<StyledPosition key={position.name}>
<StyledPositionName>
<Link to={position.link} activeClassName="active">
{position.name}
</Link>
</StyledPositionName>
</StyledPosition>
))}
</GlobalContainer>
</StyledPositions>
}
export const query = graphql`
query MyQuery {
allLever {
edges {
node {
name: text
categories {
commitment
location
team
}
link: applyUrl
}
}
}
}
`
export default Positions
Note: it's better to avoid using index as key so you can use another field
Keep in mind that you barely need to change your previous structure (because of some destructuring + aliasing) but, what's important is that page queries, only work in pages (hence the name) so I'm assuming that Positions is a separate page. Otherwise, you will need to pass down the props from the page component.
Because of the aliasing, applyUrl will be aliased as link and text to name, so your previous structure will work exactly in the same way.

How to make a toggler to translate a site using redux

I've been working for the past two days on a task which is adding the capability to translate a whole website from English to Spanish when the user selects the toggle button, However, I'm really new into Redux (use it once on a completely different project). The people who gave me the code already configured the reducers, I just need to read the status on each component.
I've tried using this code on one of the components:
const store = createStore(reducer);
store.dispatch({
type: 'TOGGLE-LANG'
});
store.subscribe(() => console.log(store.getState()));
However, it is still has something missing, and at this point, I'm completely lost and I would love to have some guidance to know what to do.
I created a Gist with all the code involved in this task, It has commented on what's the expected behavior
[Gist Link] : https://gist.github.com/ManudeQuevedo/12cd63cf7431b5ec9b982a37391b7c56
Currently, there are no errors, it is recognizing the reducer (Lang), but I would love to know how to make it actionable in the other components that need to be translated. Thanks in advance!
You can use i18n. Replacing the text contents on your website to keys, and adding a map matching the keys to different languages.
For example, before you have
<div>
name:
</div>
Now it will be like:
<div>
{t('name')}
</div>
Here is the link of an example project
At the end this is what I did:
lang.js (reducer)
import { fromJS } from 'immutable';
const initState = fromJS({
value: 'en',
translations: {
en: {
component: {
title: 'Mobile Connectivity',
subtitle: 'Smart Messaging'
}
},
es: {
component: {
title: 'Conectividad Móvil',
subtitle: 'Mensajería Inteligente'
}
},
}
});
export default (state = initState, action) => {
switch (action.type) {
case "CHANGE_LANGUAGE":
return {
...state,
value: action.payload
};
default:
return state;
}
};
and I implemented it by destructuring it on this component like so:
[Gist][1]
I added it to a gist because it's a long code so it would be more readable like that.

Base questions with Serenity/JS?

I have just started looking into switching to Serenity/JS and wanted to know if it's best practice to have base Questions/Tasks?
There are many times I will want to check if a field is blank or has an error, so I created a 'base Question' to achieve this:
Base Question
import { Is, See, Target, Task, Wait, Value, Attribute } from 'serenity-js/lib/screenplay-protractor';
import { equals, contains } from '../../../support/chai-wrapper';
import { blank } from '../../../data/blanks';
export class InputFieldQuestion {
constructor(private inputField: Target) { }
isBlank = () => Task.where(`{0} ensures the ${this.inputField} is blank`,
Wait.until(this.inputField, Is.visible()),
See.if(Value.of(this.inputField), equals(blank))
)
hasAnError = () => Task.where(`{0} ensures the ${this.inputField} has an error`,
See.if(Attribute.of(this.inputField).called('class'), contains('ng-invalid'))
)
}
I then create classes specific to the scenario but simply extend the base question:
import { LoginForm } from '../scenery/login_form';
import { InputFieldQuestion } from './common';
class CheckIfTheUsernameFieldQuestion extends InputFieldQuestion {
constructor() { super(LoginForm.Username_Field) }
}
export let CheckIfTheUsernameField = new CheckIfTheUsernameFieldQuestion();
The beauty of node exports allow me to export an instantiated question class for use in my spec.
I just wanted to know if I am abusing the Serenity/JS framework or if this is okay? I want to establish a good framework and wanted to ensure I am doing everything to best practice. Any feedback is appreciated!
Sure, you could do it this way, although I'd personally favour composition over inheritance.
From what I can tell, you're designing standardised verification Tasks, which you want to use as follows:
actor.attemptsTo(
CheckIfTheUsernameField.isBlank()
)
You could accomplish the same result with a slightly more flexible design where a task can be parametrised with the field to be checked:
actor.attemptsTo(
EnsureBlank(LoginForm.Username_Field)
)
Where:
const EnsureBlank = (field: Target) => Task.where(`#actor ensures that the ${field} is blank`,
Wait.until(field, Is.visible()),
See.if(Value.of(field), equals(blank)),
);
Or, to follow the DSL you wanted:
const EnsureThat = (field: Target) => ({
isBlank: () => Task.where(`#actor ensures that the ${field} is empty`,
Wait.until(field, Is.visible()),
See.if(Value.of(field), equals(blank)),
),
hasError: () => Task.where(`#actor ensures that the ${field} has an error`,
See.if(Attribute.of(this.inputField).called('class'), contains('ng-invalid')),
),
});
Which can be used as follows:
actor.attemptsTo(
EnsureThat(LoginForm.Username_Field).isBlank()
);
Hope this helps!
Jan

DraftJs creating simplest possible custom block

what would be the easiest way to implement custom block in Draft?
At the moment I'm using this function for default blocks
editorToggleBlockType = (blockType) => {
this.onChange(
RichUtils.toggleBlockType(
this.state.editorState,
blockType
)
);
}
then I can apply custom class using blockStyler
blockStyler = (block) => {
if (block.getType() === 'unstyled') {
return 'paragraph';
} else {
return `custom-${block.getType()}`;
}
}
Sadly blockType accepts only default types like blockquote, ol, code-block etc. and on custom type gives me an error.
Uncaught TypeError: Cannot read property 'wrapper' of undefined
My question is - how to force editor to accept custom block types so I can apply className to them? Thank you.
You need to define it in blockRenderMap.
From the docs:
const blockRenderMap = Immutable.Map({
'atomic': {
// the docs use 'MyCustomBlock', but I changed it to 'atomic' to make it easier to follow.
// element is used during paste or html conversion to auto match your component;
// it is also retained as part of this.props.children and not stripped out
element: 'section',
wrapper: <MyCustomBlock {...this.props} />
}
});
// Include 'paragraph' as a valid block and updated the unstyled element but
// keep support for other draft default block types
const extendedBlockRenderMap = Draft.DefaultDraftBlockRenderMap.merge(blockRenderMap);
class RichEditor extends React.Component {
render() {
return (
<Editor
...
blockRenderMap={extendedBlockRenderMap}
/>
);
}
}
Rather confusingly, all this does is wrap your custom block in whatever is specified in the wrapper key. The actual block is then rendered by blockRendererFn, as in the docs:
function myBlockRenderer(contentBlock) {
const type = contentBlock.getType();
if (type === 'atomic') {
return {
component: MediaComponent,
editable: false,
props: {
foo: 'bar',
},
};
}
}
// Then...
import {Editor} from 'draft-js';
class EditorWithMedia extends React.Component {
...
render() {
return <Editor ... blockRendererFn={myBlockRenderer} />;
}
}
If we follow this example verbatim, you'd get a block that looked something like:
...
<MyCustomBlock>
<MediaComponent />
</MyCustomBlock>
...
And your className from blockStyleFn would get passed to MyCustomBlock, so you can pass it down to whichever native DOM node you like. That's also why you were getting the TypeError -- DraftJS couldn't find your custom block in blockRenderMap!
I hope this answers your question. DraftJS can be confusing, but it's a very powerful framework for building RTEs.

Categories