New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[fusion-cli#645] Improved Translation API via Hooks #340
Comments
Previous discussions (fusionjs/fusion-cli#713 and fusionjs/fusion-cli#643). Background<Translate id="greeting" /> The above does 2 things
If a translation is not found, you don't know about it until the server attempts to grab that translation (on a render request or when a dynamic chunk is requested). There's no validation here, so it really just returns ProposalIn supporting dynamic translation keys, we could potentially keep these 2 objectives together, but I think it would be simpler and make a more intuitive API to separate them. // We can statically validate that this function is passed an array of static values
// (with some wildcard or other DSL)
staticAnalysisFunction(['static-key', 'dynamic-key.*']);
// No validation needed on this
runtimeTranslation(`dynamic-key.${foo}`); The API would look like: const translate = useTranslations(['greetings.*']);
translate('greetings.hello'); A custom hook lends well to this pattern since it ties one function call to a return function.
Not much else needs to change. The manifest includes all static translation keys as well as any wildcard values. When the server attempts to get translations for a chunk, it just needs to be aware of wildcard values and use them to match against all of the loaded translation keys. The plugin middleware again just returns Full example: function Component() {
const translate = useTranslations(['greetings.*']);
return (
<h1>
translate('greetings.hello')
</h1>
);
} Full disclosureThis would be possible since the useTranslations(['greetings.*']);
const ctx = useContext(FusionContext);
const i18n = useService(I18nToken).from(ctx);
i18n.translate('greetings.hello'); |
I think this looks good. The only downside is we don't have any validation checking that the runtime values match the dynamic keys specified by the hook. |
Yeah that's true. You could also technically do this: const translate = useTranslations(['greetings.*']);
translate('something-completely-different'); |
This is probably bad, but could we do some scoping? const [greetings, hello] = useTranslations(['greetings.*', 'hello.*]);
<div>{greetings.a}</div>
<div>{hello.thing({data: 'test'})}</div> |
That's pretty interesting. The problem might be that we'd be converting a flat object whose keys can contain |
In the following case, I think the developer-supplied translation matcher is more or less entirely redundant: const translate = useTranslations(["greetings.*"]);
translate(`greetings.${expression}`); I think it stands to reason that if a convenient matcher exists, then an equivalent template literal could be used. It seems a bit pointless to require developers to supply a matcher themselves. This is just extra code that has to be updated in two places. In my view, the only benefit would be more granular matchers such as Suspense should empower us to have an API that "just works", at the expense of possible request waterfalls. However, manual, developer-provided translation matchers and/or automatic, best-effort static analysis can be used to alleviate this. Before Suspense, it would just mean if a translation is used before being fetched, it would return undefined. I think this is an OK outcome if we have more aggressive buildtime/runtime checks to avoid this in common scenarios. Ultimately, having an API that "just works" would be really nice. I think a lint rule should be sufficient to guard against the worst footgun, which would be: const translate = useTranslations();
translate(expression); |
This is interesting, I wonder if we could somehow get good static types for an API like this. Maybe generated typedefs? |
I think we need to check that assumption with consumers. This also makes it more difficult to do more complex optimizations. For example: const translate = useTranslations(["cities.*.areas.*]);
return <div>Welcome to {translate(`cities.${props.key}`)}</div> If you were purely relying on the translate call, it would require loading everything under each city, when you only need the areas. Now you could potentially refactor that component to be: const translate = useTranslations();
return <div>Welcome to {translate(`cities.${props.city}.areas.${props.area}`)}</div> And that is arguably cleaner, but maybe the data comes from a backend with the full translation key, not split into city and area, and the FE may not want to deal with parsing the key into constituent parts as that adds complexity. etc etc. |
yea potentially. Although I'm not a huge fan of the aesthetics of this API. But maybe we could come up with something inspired by it. I was also trying to think of some way where we could get rid of static analysis and replace it all with code generation, but it gets ugly with nested keys and doesn't really work. But type generation could be possible. |
I think what I'm seeing is there are some cases where static analysis completely fails to deliver the fewest possible translations ( Each of these methods has tradeoffs, but I see 3 patterns that are important to allow. Hopefully these APIs are suggestive of whether they're including translations in the bundle or not. Static
<Translate id="static"/> Dynamic, include in bundle
const translate = useTranslations();
const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
{days.map(day => (
<div>{translate(`weekday.${day}`)}</div>
))} Dynamic, fetch at runtime
const Map = React.lazy(() => import('./map.js'));
function City() {
const translated = translationResource.read(`cities.${props.city}`);
return (
<div>
<div>Welcome to {translated}</div>
<Map city={props.city}/>
</div>
);
}
export default () => (
<Suspense>
<City/>
</Suspense>
); |
Is there any reason to not uplift the const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
{days.map(day => (
<Translate id={`weekday.${day}`} />
))} |
We could do this, but there will ultimately be a need for a hook (or some non component API) for translations in cases where the user needs to work with the translated string directly. For example, if you have a translated string for a native dom attribute (input placeholder for example). |
This issue was migrated from fusionjs/fusion-cli#645 and was originally reported by @ganemone.
We should investigate if we can improve our translations API using hooks.
Type of issue
Feature Request
Description
The current API for working with raw translated strings is a bit cumbersome, as it requires duplicating the translation keys, once in the
withTranslations
call, and once in thetranslate
call. This is further described here: fusionjs/fusion-cli#643We should investigate various options of improving this API, potentially taking advantage of hooks.
The text was updated successfully, but these errors were encountered: