cm

January 9, 2024

i18n and other new features in React-Natural-Typing-Effect v2.0.0

One of the things I find fun about making front end libraries is that changes to a library are often highly visible and obvious. Version 2.0.0 of my React-Natural-Typing-Effect NPM module is no different in that it now supports a measure of internationalization (note: the below sentences are all DuckDuckGo translations of "My name is JS Fiddle. What is your name?"; I did some testing in JS Fiddle, but definitely don't know these languages πŸ˜…):
v2-modes-O9jZWM5DK6.gif

Under the hood, this is achievable through a combination of the Web API's "Intl.Segmenter" and JavaScript's String method "normalize":Β 
export const getNewIntlSegments = (lang: string[], textString: string): Intl.Segments => {
  const segmenter = new Intl.Segmenter(lang, defaultSegementerOptions);
  return segmenter.segment(textString);
};

export const normalizeText = (text: string, opt: string): string => {
  return text.normalize(opt);
};

export const decomposeText = (text: string, parentKey: number): Letter[] => {
  const normalizedText = normalizeText(text, 'NFD');
  const particles = normalizedText.split('');
  const decomposedText: Letter[] = particles.map((letter) => {
    return {
      parentKey: parentKey,
      letter: letter
    }
  });
  return decomposedText;
};

export const recomposeText = (text: string[], parentKey: number): Letter => {
  const singleString = text.reduce((a,b)=>a+b, '');
  const normalizedText = singleString.normalize('NFC');
  return {
    parentKey: parentKey,
    letter: normalizedText
  };
};

Above you can see the helper functions I implemented. "getNewIntlSegments" returns a Segments object based on the language and segmenter options provided. Because my library simulates typing I hardcoded the "granularity" option to be "grapheme" rather than "word" or "sentence". Utilizing the Segmenter makes splitting arbitrary unicode strings easier, because I no longer have to account for the many international edge cases that wouldn't be covered by using the split method, even if my regex skills were outstanding:
arbitraryTextString.split(/regex/);
You can see a few differences in my comparison of different strings being segmented by split, Segmenter, as well as using the normalize method here in my JS Fiddle. The normalize method used in my "decomposeText" and "recomposeText" functions are important to give a more realistic typing simulation of certain unicode characters that are mapped to their constituent parts within the unicode planes. An example makes this more clear:Β 
  • In Korean the unicode character "뭐" is mapped to the constituent characters "ᄆ" and "α…―". On a typical Korean keyboard a user might type first "ᄆ" and then "α…―" to end up with the final "뭐".Β 

My function "decomposeText" takes the unicode character and decomposes it using JavaScript's String.prototype.normalize() method with the "NFD" option (the "D" stands for decompose). By decompose we specifically mean the character's unicode code point value is changed from a single value to the two (or more) code point values that are mapped to it. So then we can call the split method on the decomposed string to get its parts. For a simple example of it all working together:
const segmenter = new Intl.Segmenter('ko',{granularity:'grapheme'});
const segmentsArray = [...segmenter.segment('α„Œα…¦ 아름은 JS Fiddle압나ᄃᅑ. α„‹α…΅α„…α…³α†·α„‹α…΅ 뭐에요?')];
// > [{segment: 'α„Œα…¦', index: 0, input: 'α„Œα…¦ 아름은 JS Fiddle압나ᄃᅑ. α„‹α…΅α„…α…³α†·α„‹α…΅ 뭐에요?'},...]

const firstSegment = segmentsArray[0].segment;
// > 'α„Œα…¦'
const firstSegmentNormalized = firstSegment.normalize('NFD');
// looks the same, but it's not > 'α„Œα…¦'
const firstSegmentParts = firstSegmentNormalized.split('');
// > ['α„Œ', 'α…¦']

My function "recomposeText" is called later in the typing simulation in order to put the parts back together. Because these are unicode code point values we can simple concatenate the part strings and then call normalize with the "NFC" option ("C" referring to composition) on the resulting string. To reverse the simple example above:
const concatenatedParts = firstSegmentParts[0] + firstSegmentParts[1];
// > 'α„Œ α…¦'
const backToFirstSegmentNormalized = concatenatedParts.normalize('NFC');
// > 'α„Œα…¦'

All of my examples are in Korean because they make the use case here pretty clear and also because I lived there for a few years so I have a little familiarity with it (emphasis on "little"!).

Another big new feature is a "isRepeated" option for when you want to annoy users. I'm just kidding of course, but it does remind me of the old Marquee HTML element and you'd do well to take heed of the usability problems outlined here and use it sparingly and with a healthy pause between repeats so it's easy to read and not so distracting (this is specifiable in milliseconds).

The last big new feature I want to mention is the "pause" prop. I finally refactored the "typewriter" function that previously was using a JavaScript "for" loop to iterate over each typed letter and pause each iteration with a "setTimeout" callback in a Promise. While this worked, it wasn't possible to stop or restart the for loop. In order to make it possible I created a Generator function. Here's a simplified example:
// Creates Letters one at a time.
  const typerGenerator = function*(): Generator<void, void, boolean> {
    if (textString) {
      const segments = getNewIntlSegments(lang, textString);

      for (const {segment} of segments) {
        const newLetter: Letter = {'parentKey': key, 'letter': segment};
        setLetter(newLetter);
        yield;
      }
    }
  };

Notice the "yield" keyword. I initialize the typerGenerator by calling it and storing it in state as "currentTyperGen". This ensures there is only one generator being used at a time while also allowing me to call its next method. So whenever I iterate over the next letter of the string React waits for the timeout and then sets the new letter which triggers the new letter React "useEffect" and calls the generator's next method again, and so on. Now when I pass the "isPaused" prop (boolean) as "true", I can do a simple check in the "useEffect" and refrain from calling the next letter until "isPaused" is "false" again. The ability to pause was actually integral to the recomposition of decomposable characters too, because I needed a way to inject the additional characters into the array of Letters (Letters are a particular data type used in my component) in the LetterSpanner child component before the parent component would continue the typing effect. I utilize an Iterator for these additional characters within the child component.

In addition to the above changes I also removed the custom class and style attribute options and their related logic while adding an optional "id" prop. Targeting the parent HTML element is likely sufficient to do any custom styling (of course, if you find it's not just open a Github issue!). I added unit tests for all of the new helper functions as well as component tests for all components and their props, so tests now number two test suites with twenty tests. Source code for React-Natural-Typing-Effect can be found here: https://github.com/cipherphage/React-Natural-Typing-Effect. It is by no means finished or perfect, but it has come a long way in a fairly short time. Please don't be shy about providing feedback or opening Github issues!

About cm

Das Lied schlΓ€ft in der Maschine

Github:Β  https://github.com/cipherphage

My NPM module: https://www.npmjs.com/package/react-natural-typing-effect

Published articles:Β  https://aiptcomics.com/author/julianskeptic