frintjs / frint

Modular JavaScript framework for building scalable and reactive applications
https://frint.js.org/
MIT License
755 stars 34 forks source link

Proposal: frint-intl #385

Open fahad19 opened 6 years ago

fahad19 commented 6 years ago

(Intended to be done outside of this repository)

Very much a work in progress, and needs to be refined further as more feedback keeps coming in.

Todo


Background

To unify l10n/i18n usage in FrintJS Apps, a common API can be considered, instead of having different variations spread across multiple Child Apps.

Generic requirements

Proposed packages

The requirements can be handled by these proposed packages below.

The data structure for labels is expected to be like this:

import { createModel, createCollection, Types } from 'frint-data';

const Label = createModel({
  schema: {
    // unique ID
    name: Types.string, 

    // ICU Message Syntax
    value: Types.string, 
  },
});

const Labels = createCollection({
  model: Label,
});

Read more about ICU Message Syntax here: http://userguide.icu-project.org/formatparse/messages

frint-intl

Exports:

Expected interface (nothing finalized of course):

class IntlService {
  constructor(options) {
    // options.data: Labels
  }

  format(key, ...formatWithData) {
    return String;
  }
}

frint-intl-react

React components for embedding at JSX level:

import React from 'react';
import { Format } from 'frint-intl-react';

function MyComponent(props) {
  const formatData = {
    brand: 'CTNL',
  };

  return (
    <div>
      <Format name="homepage.title" data={formatData} />
    </div>
  );
}

Or, format a custom ICU syntax message at component-level:

<Format message="ICU formatted text here..." />

frint-intl-cli

This will expose a $ frint intl subcommand in CLI, via which we can find out about unused labels in our code base:

$ frint intl find-unused-labels ./**/*.js --source allLabels.json

It will first load all the label names as available in allLabels.json file, and then execute a search in all JS files matching the pattern ./**/*.js. If there are any labels usage found that do no exist in allLabels.json, it will print line with file name and line number in console.

Usage

To set up your App:

import { createApp } from 'frint';
import { IntlService } from 'frint-intl';

const App = createApp({
  name: 'MyRootApp',
  providers: [
    {
      name: 'intl',
      useFactory() {
        return new IntlService({
          data: [
            { 
              name: 'homepage.title',
              value: 'Welcome to {{brand}}!',
            },
          ],
        });
      },
    },
  ],
});

In your component, now you can do:

import { Format } from 'frint-intl-react';

function () {
  return (
    <Format 
      name="homepage.title" 
      data={{brand: 'Foo'}} 
    />;
  );
}
fahad19 commented 6 years ago

Repository created here: https://github.com/frintjs/frint-intl

viacheslaff commented 6 years ago

Good idea! I like the usage of standard internationalization format and using DI to supply IntlService.

I have a concern though regarding whether implementation of the translations should be reactive or not. By reactive I mean the ability of <Format /> component to subscribe and react to changes in translations vs just rendering once and staying the same. Main concern is performance.

Reactive approach would be useful for:

  1. switching language without reloading the page (nice to have)
  2. rendering UI without translations while waiting for them i.e. with code-splitting (maybe fast, but is a poor user experience because of blinking)

Performance, especially at the time of initial rendering, is a concern because there will be hundreds of translations on a page -> hundreds of subscriptions to observables. I wonder if it'll degrade performance significantly. Maybe using React context will be more performant way to deliver IntlService. I'm thinking of doing a performance test.

juliamaksimchik commented 6 years ago

Well done, @fahad19!

Would be also nice to implement some kind of configuration object for whole IntlService. In our current solution, we have a functionality that should show label keys instead of value. That has been implemented to help content managers find an appropriate label key that used on a page.

viacheslaff commented 6 years ago

We've discussed my comment with @fahad19 offline and we decided to go for synchronous implementation as a first step. Then we'll see whether there's need for reactive implementation.

fahad19 commented 6 years ago

@juliamaksimchik: we can definitely look into this use case!