@shopify/react-i18n
Advanced tools
Comparing version 1.1.2 to 1.1.3
@@ -11,2 +11,10 @@ # Changelog | ||
## [1.1.3] - 2019-04-17 | ||
### Fixed | ||
- Fixed a number of performance issues with resolving asynchronous translations ([#659](https://github.com/Shopify/quilt/pull/659)) | ||
## [1.1.1] - 2019-04-12 | ||
### Changed | ||
@@ -13,0 +21,0 @@ |
@@ -5,2 +5,3 @@ import * as React from 'react'; | ||
export declare const I18nContext: React.Context<I18nManager | null>; | ||
export declare const I18nParentsContext: React.Context<I18n | null>; | ||
export declare const I18nIdsContext: React.Context<string[]>; | ||
export declare const I18nParentContext: React.Context<I18n | null>; |
@@ -6,2 +6,3 @@ "use strict"; | ||
exports.I18nContext = React.createContext(null); | ||
exports.I18nParentsContext = React.createContext(null); | ||
exports.I18nIdsContext = React.createContext([]); | ||
exports.I18nParentContext = React.createContext(null); |
import * as React from 'react'; | ||
import { I18n } from './i18n'; | ||
import { RegisterOptions } from './manager'; | ||
export declare function useI18n({ id, fallback, translations, }?: Partial<RegisterOptions>): [I18n, React.ComponentType<{ | ||
declare type Result = [I18n, React.ComponentType<{ | ||
children: React.ReactNode; | ||
}>]; | ||
export declare function useSimpleI18n(): I18n; | ||
export declare function useI18n(options?: Partial<RegisterOptions>): Result; | ||
export {}; |
@@ -5,6 +5,6 @@ "use strict"; | ||
var React = tslib_1.__importStar(require("react")); | ||
var react_hooks_1 = require("@shopify/react-hooks"); | ||
var i18n_1 = require("./i18n"); | ||
var context_1 = require("./context"); | ||
function useI18n(_a) { | ||
var _b = _a === void 0 ? {} : _a, id = _b.id, fallback = _b.fallback, translations = _b.translations; | ||
function useI18n(options) { | ||
var manager = React.useContext(context_1.I18nContext); | ||
@@ -14,38 +14,71 @@ if (manager == null) { | ||
} | ||
var parentI18n = React.useContext(context_1.I18nParentsContext); | ||
var parentIds = parentI18n ? parentI18n.ids || [] : []; | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
var ids = React.useMemo(function () { return (id ? tslib_1.__spread([id], parentIds) : parentIds); }, tslib_1.__spread([ | ||
id | ||
], parentIds)); | ||
var registerOptions = React.useRef(options); | ||
if (shouldRegister(registerOptions.current) !== shouldRegister(options)) { | ||
throw new Error('You switched between providing registration options and not providing them, which is not supported.'); | ||
} | ||
// Yes, this would usually be dangerous. But just above this line, we check to make | ||
// sure that they never switch from the case where `options == null` to `options != null`, | ||
// so we know that a given use of this hook will only ever hit one of these two cases. | ||
/* eslint-disable react-hooks/rules-of-hooks */ | ||
if (options == null) { | ||
return useSimpleI18n(manager); | ||
} | ||
else { | ||
return useComplexI18n(options, manager); | ||
} | ||
/* eslint-enable react-hooks/rules-of-hooks */ | ||
} | ||
exports.useI18n = useI18n; | ||
function useComplexI18n(_a, manager) { | ||
var id = _a.id, fallback = _a.fallback, translations = _a.translations; | ||
var parentIds = React.useContext(context_1.I18nIdsContext); | ||
// Parent IDs can only change when a parent gets added/ removed, | ||
// which would cause the component using `useI18n` to unmount. | ||
// We also don't support the `id` changing between renders. For these | ||
// reasons, it's safe to just store the IDs once and never let them change. | ||
var ids = react_hooks_1.useLazyRef(function () { return (id ? tslib_1.__spread([id], parentIds) : parentIds); }); | ||
if (id && (translations || fallback)) { | ||
manager.register({ id: id, translations: translations, fallback: fallback }); | ||
} | ||
var _c = tslib_1.__read(React.useState(function () { | ||
var translations = manager.state(ids).translations; | ||
return new i18n_1.I18n(translations, manager.details, ids); | ||
}), 2), i18n = _c[0], setI18n = _c[1]; | ||
var _b = tslib_1.__read(React.useState(function () { | ||
var translations = manager.state(ids.current).translations; | ||
return new i18n_1.I18n(translations, manager.details); | ||
}), 2), i18n = _b[0], setI18n = _b[1]; | ||
var i18nRef = React.useRef(i18n); | ||
React.useEffect(function () { | ||
return manager.subscribe(ids, function (_a, details) { | ||
return manager.subscribe(ids.current, function (_a, details) { | ||
var translations = _a.translations; | ||
setI18n(new i18n_1.I18n(translations, details, ids)); | ||
var newI18n = new i18n_1.I18n(translations, details); | ||
i18nRef.current = newI18n; | ||
setI18n(newI18n); | ||
}); | ||
}, [ids, manager]); | ||
var ShareTranslations = React.useMemo(function () { | ||
// We use refs in this component so that it never changes. If this component | ||
// is regenerated, it will unmount the entire tree of the previous component, | ||
// which is usually not desirable. Technically, this does leave surface area | ||
// for a bug to sneak in: if the component that renders this does so inside | ||
// a component that blocks the update from passing down, nothing will force | ||
// this component to re-render, so no descendants will get the new ids/ i18n | ||
// value. Because we don't actually have any such cases, we're OK with this | ||
// for now. | ||
var shareTranslationsComponent = react_hooks_1.useLazyRef(function () { | ||
return function ShareTranslations(_a) { | ||
var children = _a.children; | ||
return (React.createElement(context_1.I18nParentsContext.Provider, { value: i18n }, children)); | ||
return (React.createElement(context_1.I18nIdsContext.Provider, { value: ids.current }, | ||
React.createElement(context_1.I18nParentContext.Provider, { value: i18nRef.current }, children))); | ||
}; | ||
}, [i18n]); | ||
return [i18n, ShareTranslations]; | ||
}); | ||
return [i18n, shareTranslationsComponent.current]; | ||
} | ||
exports.useI18n = useI18n; | ||
function useSimpleI18n() { | ||
var manager = React.useContext(context_1.I18nContext); | ||
if (manager == null) { | ||
throw new Error('Missing i18n manager. Make sure to use an <I18nContext.Provider /> somewhere in your React tree.'); | ||
} | ||
var i18n = React.useContext(context_1.I18nParentsContext) || new i18n_1.I18n([], manager.details); | ||
return i18n; | ||
function useSimpleI18n(manager) { | ||
var i18n = React.useContext(context_1.I18nParentContext) || new i18n_1.I18n([], manager.details); | ||
return [i18n, IdentityComponent]; | ||
} | ||
exports.useSimpleI18n = useSimpleI18n; | ||
function IdentityComponent(_a) { | ||
var children = _a.children; | ||
return React.createElement(React.Fragment, null, children); | ||
} | ||
function shouldRegister(_a) { | ||
var _b = _a === void 0 ? {} : _a, fallback = _b.fallback, translations = _b.translations; | ||
return fallback != null || translations != null; | ||
} |
@@ -14,3 +14,2 @@ /// <reference types="react" /> | ||
readonly translations: TranslationDictionary[]; | ||
readonly ids?: string[] | undefined; | ||
readonly locale: string; | ||
@@ -31,3 +30,3 @@ readonly pseudolocalize: boolean | string; | ||
readonly isLtrLanguage: boolean; | ||
constructor(translations: TranslationDictionary[], { locale, currency, timezone, country, pseudolocalize, onError, }: I18nDetails, ids?: string[] | undefined); | ||
constructor(translations: TranslationDictionary[], { locale, currency, timezone, country, pseudolocalize, onError, }: I18nDetails); | ||
translate(id: string, options: TranslateOptions, replacements?: PrimitiveReplacementDictionary): string; | ||
@@ -34,0 +33,0 @@ translate(id: string, options: TranslateOptions, replacements?: ComplexReplacementDictionary): React.ReactElement<any>; |
@@ -24,7 +24,6 @@ "use strict"; | ||
var I18n = /** @class */ (function () { | ||
function I18n(translations, _a, ids) { | ||
function I18n(translations, _a) { | ||
var locale = _a.locale, currency = _a.currency, timezone = _a.timezone, country = _a.country, _b = _a.pseudolocalize, pseudolocalize = _b === void 0 ? false : _b, onError = _a.onError; | ||
var _this = this; | ||
this.translations = translations; | ||
this.ids = ids; | ||
this.getCurrencySymbol = function (currencyCode) { | ||
@@ -31,0 +30,0 @@ var currency = currencyCode || _this.defaultCurrency; |
export { I18nManager, ExtractedTranslations } from './manager'; | ||
export { I18nContext } from './context'; | ||
export { I18n } from './i18n'; | ||
export { useI18n, useSimpleI18n } from './hooks'; | ||
export { useI18n } from './hooks'; | ||
export { withI18n, WithI18nProps } from './decorator'; | ||
@@ -6,0 +6,0 @@ export { translate } from './utilities'; |
@@ -11,3 +11,2 @@ "use strict"; | ||
exports.useI18n = hooks_1.useI18n; | ||
exports.useSimpleI18n = hooks_1.useSimpleI18n; | ||
var decorator_1 = require("./decorator"); | ||
@@ -14,0 +13,0 @@ exports.withI18n = decorator_1.withI18n; |
@@ -39,2 +39,4 @@ import { I18nDetails, TranslationDictionary, MaybePromise } from './types'; | ||
private translationPromises; | ||
private idsToUpdate; | ||
private enqueuedUpdate?; | ||
constructor(details: I18nDetails, initialTranslations?: ExtractedTranslations); | ||
@@ -41,0 +43,0 @@ resolve(): Promise<void>; |
@@ -15,2 +15,3 @@ "use strict"; | ||
this.translationPromises = new Map(); | ||
this.idsToUpdate = new Set(); | ||
try { | ||
@@ -82,2 +83,3 @@ for (var _b = tslib_1.__values(Object.entries(initialTranslations)), _c = _b.next(); !_c.done; _c = _b.next()) { | ||
var loading = false; | ||
var hasUnresolvedTranslations = false; | ||
var translations = ids.reduce(function (otherTranslations, id) { | ||
@@ -93,3 +95,3 @@ var e_2, _a; | ||
if (_this.translationPromises.has(translationId)) { | ||
loading = true; | ||
hasUnresolvedTranslations = true; | ||
} | ||
@@ -109,2 +111,5 @@ } | ||
} | ||
if (translationsForId.length === 0 && hasUnresolvedTranslations) { | ||
loading = true; | ||
} | ||
if (!omitFallbacks) { | ||
@@ -173,3 +178,5 @@ var fallback = _this.fallbacks.get(id); | ||
_this.asyncTranslationIds.add(translationId); | ||
_this.updateSubscribersForId(id); | ||
if (result != null) { | ||
_this.updateSubscribersForId(id); | ||
} | ||
}) | ||
@@ -180,3 +187,2 @@ .catch(function () { | ||
_this.asyncTranslationIds.add(translationId); | ||
_this.updateSubscribersForId(id); | ||
}); | ||
@@ -205,18 +211,30 @@ this_1.translationPromises.set(translationId, promise); | ||
I18nManager.prototype.updateSubscribersForId = function (id) { | ||
var e_6, _a; | ||
try { | ||
for (var _b = tslib_1.__values(this.subscriptions), _c = _b.next(); !_c.done; _c = _b.next()) { | ||
var _d = tslib_1.__read(_c.value, 2), subscriber = _d[0], ids = _d[1]; | ||
if (ids.includes(id)) { | ||
subscriber(this.state(ids), this.details); | ||
} | ||
} | ||
var _this = this; | ||
this.idsToUpdate.add(id); | ||
if (this.enqueuedUpdate != null) { | ||
return; | ||
} | ||
catch (e_6_1) { e_6 = { error: e_6_1 }; } | ||
finally { | ||
var isBrowser = typeof window !== 'undefined'; | ||
var enqueue = isBrowser ? window.requestAnimationFrame : setImmediate; | ||
this.enqueuedUpdate = enqueue(function () { | ||
var e_6, _a; | ||
delete _this.enqueuedUpdate; | ||
var idsToUpdate = tslib_1.__spread(_this.idsToUpdate); | ||
_this.idsToUpdate.clear(); | ||
try { | ||
if (_c && !_c.done && (_a = _b.return)) _a.call(_b); | ||
for (var _b = tslib_1.__values(_this.subscriptions), _c = _b.next(); !_c.done; _c = _b.next()) { | ||
var _d = tslib_1.__read(_c.value, 2), subscriber = _d[0], ids = _d[1]; | ||
if (ids.some(function (id) { return idsToUpdate.includes(id); })) { | ||
subscriber(_this.state(ids), _this.details); | ||
} | ||
} | ||
} | ||
finally { if (e_6) throw e_6.error; } | ||
} | ||
catch (e_6_1) { e_6 = { error: e_6_1 }; } | ||
finally { | ||
try { | ||
if (_c && !_c.done && (_a = _b.return)) _a.call(_b); | ||
} | ||
finally { if (e_6) throw e_6.error; } | ||
} | ||
}); | ||
}; | ||
@@ -223,0 +241,0 @@ return I18nManager; |
{ | ||
"name": "@shopify/react-i18n", | ||
"version": "1.1.2", | ||
"version": "1.1.3", | ||
"license": "MIT", | ||
@@ -38,2 +38,3 @@ "description": "i18n utilities for React handling translations, formatting, and more.", | ||
"@shopify/react-effect": "^3.0.1", | ||
"@shopify/react-hooks": "^1.1.0", | ||
"@types/hoist-non-react-statics": "^3.0.1", | ||
@@ -40,0 +41,0 @@ "hoist-non-react-statics": "^3.0.1", |
@@ -72,2 +72,4 @@ # `@shopify/react-i18n` | ||
> **Note:** `ShareTranslations` is not guaranteed to re-render when your i18n object changes. If you render `ShareTranslations` inside of a component that might block changes to children, you will likely run into issues. To prevent this, we recommend that `ShareTranslations` should be rendered as a top-level child of the component that uses `useI18n`. | ||
```tsx | ||
@@ -85,9 +87,5 @@ import * as React from 'react'; | ||
return ( | ||
<Page | ||
title={i18n.translate('ProductDetails.title')} | ||
> | ||
<ShareTranslations> | ||
{children} | ||
</ShareTranslations> | ||
</EmptyState> | ||
<ShareTranslations> | ||
<Page title={i18n.translate('ProductDetails.title')}>{children}</Page> | ||
</ShareTranslations> | ||
); | ||
@@ -125,22 +123,2 @@ } | ||
If you only need access to parent translations and/ or the various formatting utilities found on the `I18n` object, you can instead use the `useSimpleI18n` hook. This hook does not support providing any internationalization details for the component itself, but is a very performant way to access i18n utilities that are tied to the global locale. | ||
```tsx | ||
import * as React from 'react'; | ||
import {EmptyState} from '@shopify/polaris'; | ||
import {useSimpleI18n} from '@shopify/react-i18n'; | ||
export default function NotFound() { | ||
const i18n = useSimpleI18n(); | ||
return ( | ||
<EmptyState | ||
heading={i18n.translate('NotFound.heading')} | ||
action={{content: i18n.translate('Common.back'), url: '/'}} | ||
> | ||
<p>{i18n.translate('NotFound.content')}</p> | ||
</EmptyState> | ||
); | ||
} | ||
``` | ||
#### `i18n` | ||
@@ -147,0 +125,0 @@ |
93329
1912
9
373
+ Added@shopify/react-hooks@^1.1.0
+ Added@shopify/react-hooks@1.13.1(transitive)