npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@nextcapital/copy-service

v5.3.0

Published

A modern library for UI copy management.

Downloads

23

Readme

Copy Service

A UX Framework Project

Node Version npm Contributor Covenant Known Vulnerabilities Blazing Fast

A JavaScript service for copy management and internationalization (i18n). This service allows providing copy in our custom DSL, then evaluating it to an output format (eg: text, HTML, React JSX) with dynamic substitutions at runtime.

What is "copy"? Basically, any human-readable text visible in your application UI. This service helps centralize copy handling in your application, making it easier to manage. No more hardcoded strings in templates or janky regex substitutions!

Installation

npm install --save @nextcapital/copy-service

Features

  • Supports registration of copy from multiple sources, merging later copy into already registered copy.
    • Allows for concise tenant overrides of abstracted, generic copy.
  • Provides an expressive, feature-rich DSL for defining copy
    • Reference syntax to include copy in other copy, preventing duplication
    • Substitution syntax to interpolate dynamic values, including dynamic references to other copy
    • Functional syntax to handle complex formatting and logic-based substitutions
    • Supports formatting HTML tags (e.g. <strong>, <em>)
    • Support for links and other arbitrary custom formatting
  • Supports different output formats via Evaluators
    • String literals, React components (JSX), formatted HTML text, etc...
    • Need something else? Build your own evaluator!
  • Works natively on node and in webpack/browsers.
    • Great for server or client side rendering!
  • Full support for internationalization (i18n)
    • Switch languages dynamically at runtime
    • Language hierarchy with automatic fallbacks
  • High performance:
    • Copy isn't parsed until it is used. Once it is parsed, the resulting AST is cached.
    • Whenever an evaluator sees that an AST will always produce the same result, it will cache it, preventing the need for evaluation on subsequent requests.
    • In fact, if the evaluation for even part of an AST can be cached, it will be.
    • A single shared copy service can be used with many evaluators
    • Overall, the system is designed to minimize array and object allocations. This reduces GC pressure when rendering lots of copy on a page.

Comparison to i18next / i18next-react

Copy Service and i18next/i18next-react are broadly comparable. Both are libraries for defining a copy store, including copy by references to keys in this store, and evaluating an AST to interpolated/formatted copy.

The i18next library has a few advantages:

  • It is older and more established, with a larger community
  • It handles automatic language detection
  • It handles automatically loading copy and has fallback behavior while loading

However, we think that this is where the advantages end:

  • Copy Service allows using multiple evaluators (JSX, HTML) with a single copy store. The i18next library mostly outputs to strings, with a series of hacks (see the i18next-react Trans component) used to produce formatted React JSX. Copy Service can render directly to formatted JSX, HTML, plaintext strings, etc...
  • If you are a React developer, Copy Service is significantly easier to use with formatted copy since it renders directly to JSX, has built in support for HTML tags, and allow interpolation of arbitrary JSX. No more fiddling with indexes in your copy with the Trans component!
  • Our DSL is significantly more expressive. A single copy string can contain multiple plurals and formatter functions: whereas i18next is limited to one per copy key (forcing the use of deep references to add more than one). Our ternary syntax is quite powerful!
  • Copy Service is significantly smaller in your bundle. While we have not definitively benchmarked, we expect that it is likely also significantly faster. Copy Service has been extensively performance optimized.
  • Language detection, automatic copy loading, and fallbacks for missing/loading copy can be pretty easily implemented by a developer using Copy Service.

Structure

The Copy Service project is made up of two primary parts: the CopyService class and the Evaluator subclasses. The CopyService class handles registration of copy and parsing copy into an AST that any Evaluator subclass can consume. Evaluator subclasses are responsible for processing requests for copy, converting the AST into formatted copy with interpolated substitutions.

Evaluators

Currently, this project provides three evaluators:

  • HtmlEvaluator returns copy formatted as a string of HTML elements.
  • PlainTextEvaluator returns copy formatted as string literals.
  • ReactEvaluator returns copy formatted as React components (JSX).

Need something else? Feel free to build a custom Evaluator subclass!

Usage

import { CopyService } from '@nextcapital/copy-service';
import HtmlEvaluator from '@nextcapital/copy-service/HtmlEvaluator';
import PlainTextEvaluator from '@nextcapital/copy-service/PlainTextEvaluator';
import ReactEvaluator from '@nextcapital/copy-service/ReactEvaluator';

import copy from './some/path/to/copy.json'; // or load from a URL async

const copyService = new CopyService();
copyService.registerCopy(copy);

const htmlEvaluator = new HtmlEvaluator(copyService);
const textEvaluator = new PlainTextEvaluator(copyService);
const reactEvaluator = new ReactEvaluator(copyService);

const htmlCopy = htmlEvaluator.getCopy('some.copy.key', { some: 'substitutions' });
const stringCopy = textEvaluator.getCopy('some.copy.key', { some: 'substitutions' });
const jsxCopy = reactEvaluator.getCopy('some.copy.key', { some: 'substitutions' });

Copy Keys

Copy keys are a key path in a registered copy file to a specific piece of copy.

Example

// copy.json
{
    "account": {
        "accountNumber": "Account Number",
        "characters": {
            "checking": "Checking",
            "savings": "Savings"
        }
    }
}
  • The copy key for 'Account Number' is account.accountNumber.
  • The copy key for 'Checking' is account.characters.checking.
  • Attempting to use account as a copy key will result in an error: only paths to leaves are copy keys.

Substitutions

The copy service allows for dynamic interpolation of values into the final evaluated copy. The syntax (defined below) will reference keys on the passed substitutions object to find the correct value to interpolate.

Note: Most substitutions do not require calculations and can be directly provided on the substitutions object. However, some values require expensive calculations (i.e. take a long time to calculate). For this, there are two solutions. First, instead of passing a substitution object, you can pass a function returning a substitution object. The function will not be invoked until a piece of copy requiring a substitution is requested. The substitution object returned by this function will be cached and reused for subsequent substitutions. This allows multiple parts of a copy key to use the same cached value without double-calculating it. Second, a function can be passed as an individual substitution using the syntax defined below, which will not be called until the piece of copy is requested.

Example

{
    "value": 4000,
    "nested": {
        "displayValue": "4,000.00"
    }
}

The valid substitutionKey paths for these attributes are value and nested.displayValue.

Writing Copy

Syntax

| Syntax | Description | | -------------------------------------------- | ------------------------------------------------------------ | | ${some.copy.key.to.reference} | Interpolates another piece of copy. Works recursively. | | ${..relative.reference} | References support dot syntax (like file paths) for relative references. | | #{substitutionKey} | Interpolates a substitution. | | #{substitution.deep.key} | Interpolates a substitution using a deep key reference. | | %{substitutionKey} | Grabs a copy key reference from the substitutions and interpolates the referenced copy. Relative references are not supported here. | | *{left}{right}{substitutionKey} | Interpolates left or right copy based on the value of the substitution (i.e. basic logic switch). If the substitution evaluates to a truthy value or the number 1, the left copy will be interpolated. If the substitution evaluates to a falsy value or the number 0, the right copy will be interpolated. | | ^{COPY}{substitutionKey}* | Passes COPY to a function that returns copy. Allows for extremely flexible interpolation and evaluation of copy. Useful for inserting links, formatting, etc... | | ^{COPY}{substitutionKey}[arg1, arg2, ...]* | Passes COPY and arguments to a function that returns copy. Arguments are passed as string literals (e.g. the boolean true will be passed as the string 'true'). | | <html>COPY</html>*^ | Interpolates HTML formatting tags. Valid tags are listed below. Invalid tags will be ignored. |

* This syntax is not executed when the evaluator has the allowFunctional option set to false. These evaluators will simply return the COPY inside of the syntax tags.

Valid HTML tags are <strong>, <u>, <sup>, <sub>, <s>, <em>, <p>, <span>, <div>, <ol>, <ul>, <li>, <b>, <i>, and <u>. We reccomend that instead of using b and i tags, use strong and em in order to improve accessibility.

Examples

// copy.json
{
  "account": {
    "name": "Account",
    "value": "#{accountValue}",
    "description": "${account.characters.checking}",
    "characters": {
      "checking": "Checking Account",
      "savings": "Savings Account"
    },
    "owner": "*{Spouse}{Primary}{isSpouse}",
    "recursiveProof": "${account.description}",
    "subbedCharacter": "%{character}",
    "switchedCharacter": "*{${account.characters.checking}}{${account.characters.savings}}{characterSwitch}",
    "save": "<strong>Save</strong>",
    "cancel": "<q>Cancel</q>",
    "rollover": "^{You may rollover}[rollover]",
    "implement": "^{You are going to implement}[implement][true]",
    "wordbreak": "Step\b 3 of 10"
  },
  "relativeKeyProof": {
    "two": "in a ${.nested.three}",
    "nested": {
      "one": "You can reference keys ${..two}.",
      "three": "relative way",
    }
  },
  "link": {
    "example": "Visit our ^{${.website}}{makeLink}[https://nextcapital.com]!",
    "website": "fancy website"
  }
}
  • account.name will resolve to 'Account'.
  • account.description will resolve to 'Checking Account'.
  • account.recursiveProof will resolve to 'Checking Account'.
  • relativeKeyProof.nested.one will resolve to 'You can reference keys in a relative way.'.
  • account.value with substitutions { accountValue: 100 } will resolve to '$100'.
  • account.subbedCharacter with substitutions { character: 'account.characters.savings' } will resolve to 'Savings Account'.
  • account.owner with substitutions { isSpouse: true } will resolve to 'Spouse'.
  • account.owner with substitutions { isSpouse: false } will resolve to 'Primary'.
  • account.switchedCharacter with substitutions { characterSwitch: true } will resolve to 'Checking Account'.
  • With the HtmlEvaluator, account.save will resolve to '<strong>Save</strong>'.
  • With the PlainTextEvaluator, account.save will resolve to 'Save'.
  • With the ReactEvaluator, account.save will resolve to <strong>Save</strong>.
  • With the HtmlEvaluator, account.cancel will resolve to 'Cancel'.
  • With the PlainTextEvaluator, account.cancel will resolve to 'Cancel'.
  • With the ReactEvaluator, account.cancel will resolve to Cancel.
  • With the HtmlEvaluator, account.rollover will resolve to 'You may rollover your external IRA'.
  • With the PlainTextEvaluator, account.rollover with substitutions { rollover: (copy) => copy + 'your external IRA' } will resolve to 'You may rollover' if allowFormatting is false, and 'You may rollover your external IRA' if it is true.
  • With the ReactEvaluator, account.rollover will resolve to You may rollover your external IRA.
  • With the HtmlEvaluator, account.implement with substitutions { implement: (copy, addMore) => copy + 'your NextCapital managed account' } will resolve to 'You are going to implement your NextCapital managed account'.
  • With the HtmlEvaluator and ReactEvaluator, account.wordbreak will resolve to Step<wbr/> 3 of 10. Note: \b is custom language syntax to this project.
  • With the ReactEvaluator, link.example with substitutions { makeLink: (copy, url) => (<a href={ url }>{ copy }</a>) } will resolve to Visit our <a href="https://nextcapital.com">fancy website</a>!.

Internationalization (i18n)

We reccomend two strategies for internationalization, each with their benefits and drawbacks:

  • static:
    • The server knows the correct languages, and returns a copy file for that language that has the fallback hierarchy already applied to it locally.
    • A plain CopyService would register this copy and be used as normal.
    • This has the best performance, as copy for other languages never needs to be downloaded/included. However, you cannot switch languages at runtime without fully downloading/replacing new copy.
  • dynamic:
    • Use IntlCopyService instead of CopyService, provide the hierarchy and all language copy.
    • This allows switching languages dynamically at runtime.
    • However, it requires more bandwidth, as copy for other languages that the end user may never need is included.
    • This is probably the best solution when running natively on node (as is the case when server-side rendering).

In either case, the copy service should support all use cases for internationalization. Whenever passing in substitutions that may need i18n support (eg: formatted dates), make sure those substitutions are also properly localized.

Contributing to Copy Service

See CONTRIBUTING.md

Maintainers

Jonathan Pierce (@nc-piercej) Mike Kreiser (@nc-kreiserm)

Background

This project uses a parser-evaluator pattern on ASTs built on a defined grammar. The CopyService class contains the parser responsible for tokenizing each piece of copy and parsing it into an AST of ES6 classes representing syntax. The Evaluator class is responsible for evaluating the AST generated for a given Copy Key into copy formatted as a string literal, HTML, JSX (React component), etc.

Grammar

The grammar is not left-recursive and therefore recursive-descent (easily) parsable.

Writing an LR parser would be significantly more difficult, so best to avoid complex grammars.

COPY :: *{ RESTRAINED RESTRAINED VERBATIM } COPY | ${ VERBATIM } COPY | #{ VERBATIM } COPY | VERBATIM COPY | ^{ RESTRAINED VERBATIM } COPY | ^{ RESTRAINED VERBATIM }[ ARGUMENTS | COPY nil

RESTRAINED :: *{ RESTRAINED RESTRAINED VERBATIM } RESTRAINED | ${ VERBATIM } RESTRAINED | #{ VERBATIM } RESTRAINED | ^{ RESTRAINED VERBATIM } RESTRAINED | VERBATIM RESTRAINED | | ^{ RESTRAINED VERBATIM }[ ARGUMENTS | }{ ARGUMENTS: VERBATIM ] | VERBATIM , ARGUMENTS

ARGUMENTS :: VERBATIM , ARGUMENTS | VERBATIM ] COPY

VERBATIM :: any string that does not begin with *{, #{, ^{, or ${

NOTE: Many bothans died to bring you this grammar.