UNPKG

4.53 kBJavaScriptView Raw
1/** Copyright (c) 2018 Uber Technologies, Inc.
2 *
3 * This source code is licensed under the MIT license found in the
4 * LICENSE file in the root directory of this source tree.
5 *
6 * @flow
7 */
8
9/* eslint-env browser */
10import {FetchToken} from 'fusion-tokens';
11import {createPlugin, unescape, createToken} from 'fusion-core';
12import type {FusionPlugin, Token} from 'fusion-core';
13import {UniversalEventsToken} from 'fusion-plugin-universal-events';
14
15import type {
16 I18nDepsType,
17 I18nServiceType,
18 TranslationsObjectType,
19} from './types.js';
20
21type LoadedTranslationsType = {
22 localeCode?: string,
23 translations?: TranslationsObjectType,
24};
25function loadTranslations(): LoadedTranslationsType {
26 const element = document.getElementById('__TRANSLATIONS__');
27 if (!element) {
28 throw new Error(
29 '[fusion-plugin-i18n] - Could not find a __TRANSLATIONS__ element'
30 );
31 }
32 try {
33 return JSON.parse(unescape(element.textContent));
34 } catch (e) {
35 throw new Error(
36 '[fusion-plugin-i18n] - Error parsing __TRANSLATIONS__ element content'
37 );
38 }
39}
40
41type HydrationStateType = {
42 localeCode?: string,
43 translations: TranslationsObjectType,
44};
45export const HydrationStateToken: Token<HydrationStateType> = createToken(
46 'HydrationStateToken'
47);
48
49type PluginType = FusionPlugin<I18nDepsType, I18nServiceType>;
50const pluginFactory: () => PluginType = () =>
51 createPlugin({
52 deps: {
53 fetch: FetchToken.optional,
54 hydrationState: HydrationStateToken.optional,
55 events: UniversalEventsToken.optional,
56 },
57 provides: ({fetch = window.fetch, hydrationState, events} = {}) => {
58 class I18n {
59 locale: string;
60 translations: TranslationsObjectType;
61 requestedKeys: Set<string>;
62
63 constructor() {
64 const {localeCode, translations} =
65 hydrationState || loadTranslations();
66 this.requestedKeys = new Set();
67 this.translations = translations || {};
68 if (localeCode) {
69 this.locale = localeCode;
70 }
71 }
72 async load(translationKeys) {
73 const loadedKeys = Object.keys(this.translations);
74 const unloaded = translationKeys.filter(key => {
75 return loadedKeys.indexOf(key) < 0 && !this.requestedKeys.has(key);
76 });
77 if (unloaded.length > 0) {
78 // Don't try to load translations again if a request is already in
79 // flight. This means that we need to add unloaded chunks to
80 // loadedChunks optimistically and remove them if some error happens
81 unloaded.forEach(key => {
82 this.requestedKeys.add(key);
83 });
84 const fetchOpts = {
85 method: 'POST',
86 headers: {
87 Accept: '*/*',
88 'Content-Type': 'application/json',
89 ...(this.locale ? {'X-Fusion-Locale-Code': this.locale} : {}),
90 },
91 body: JSON.stringify(unloaded),
92 };
93 // TODO(#3) don't append prefix if injected fetch also injects prefix
94 return fetch(
95 `/_translations${
96 this.locale ? `?localeCode=${this.locale}` : ''
97 }`,
98 fetchOpts
99 )
100 .then(r => r.json())
101 .then((data: {[string]: string}) => {
102 for (const key in data) {
103 this.translations[key] = data[key];
104 this.requestedKeys.delete(key);
105 }
106 })
107 .catch((err: Error) => {
108 // An error occurred, so remove the chunks we were trying to load
109 // from loadedChunks. This allows us to try to load those chunk
110 // translations again
111 unloaded.forEach(key => {
112 this.requestedKeys.delete(key);
113 });
114 throw err;
115 });
116 }
117 }
118 translate(key, interpolations = {}) {
119 const template = this.translations[key];
120
121 if (typeof template !== 'string') {
122 events && events.emit('i18n-translate-miss', {key});
123 return key;
124 }
125
126 return template.replace(/\${(.*?)}/g, (_, k) =>
127 interpolations[k] === void 0
128 ? '${' + k + '}'
129 : String(interpolations[k])
130 );
131 }
132 }
133 const i18n = new I18n();
134 return {from: () => i18n};
135 },
136 });
137
138export default ((__BROWSER__ && pluginFactory(): any): PluginType);