Skip to content

Commit 4fc242d

Browse files
authored
Merge pull request #4701 from akshay-99/i18n-host-translations-online
FES + i18n: Load translations from a CDN
2 parents 104c0bb + 362adf8 commit 4fc242d

File tree

7 files changed

+198
-48
lines changed

7 files changed

+198
-48
lines changed

Gruntfile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ module.exports = grunt => {
189189
'src/**/*.vert',
190190
'src/**/*.glsl'
191191
],
192-
tasks: ['browserify'],
192+
tasks: ['browserify:dev'],
193193
options: {
194194
livereload: true
195195
}

contributor_docs/internationalization.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,12 @@ The easiest way to do this is to add your language code (like "de" for German, "
8282

8383
This will generate you a fresh translations file in `translations/{LANGUAGE_CODE}/`! Now you can begin populating it with your fresh translations! 🥖
8484

85-
You'll also need to add an entry for it in `translations/index.js`. You can follow the pattern used in that file for `en` and `es`.
85+
You'll also need to add an entry for it in [`translations/index.js`](../translations/index.js) and [`translations/dev.js`](../translations/dev.js). You can follow the pattern used in that file for `en` and `es`.
86+
87+
### Testing changes
88+
The bulk of translations are not included in the final library, but are hosted online and are automatically downloaded by p5.js when it needs them. Updates to these only happen whenever a new version of p5.js is released.
89+
90+
However, if you want to see your changes (or any other changes which aren't released yet), you can simply run `npm run dev` which will build p5.js configured to use the translation files present locally on your computer, instead of the ones on the internet.
8691

8792
### Further Reading
8893

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"scripts": {
55
"grunt": "grunt",
66
"build": "grunt build",
7-
"dev": "grunt browserify connect:yui watch:quick",
7+
"dev": "grunt browserify:dev connect:yui watch:quick",
88
"docs": "grunt yui",
99
"docs:dev": "grunt yui:dev",
1010
"test": "grunt",
@@ -94,7 +94,8 @@
9494
"lib/p5.min.js",
9595
"lib/p5.js",
9696
"lib/addons/p5.sound.js",
97-
"lib/addons/p5.sound.min.js"
97+
"lib/addons/p5.sound.min.js",
98+
"translations/**"
9899
],
99100
"description": "[![npm version](https://badge.fury.io/js/p5.svg)](https://www.npmjs.com/package/p5)",
100101
"bugs": {

src/core/internationalization.js

Lines changed: 140 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,107 @@
11
import i18next from 'i18next';
22
import LanguageDetector from 'i18next-browser-languagedetector';
33

4-
let resources;
5-
// Do not include translations in the minified js
4+
let fallbackResources, languages;
65
if (typeof IS_MINIFIED === 'undefined') {
7-
resources = require('../../translations').default;
6+
// internationalization is only for the unminified build
7+
8+
const translationsModule = require('../../translations');
9+
fallbackResources = translationsModule.default;
10+
languages = translationsModule.languages;
11+
12+
if (typeof P5_DEV_BUILD !== 'undefined') {
13+
// When the library is built in development mode ( using npm run dev )
14+
// we want to use the current translation files on the disk, which may have
15+
// been updated but not yet pushed to the CDN.
16+
let completeResources = require('../../translations/dev');
17+
for (const language of Object.keys(completeResources)) {
18+
// In es_translation, language is es and namespace is translation
19+
// In es_MX_translation, language is es-MX and namespace is translation
20+
const parts = language.split('_');
21+
const lng = parts.slice(0, parts.length - 1).join('-');
22+
const ns = parts[parts.length - 1];
23+
24+
fallbackResources[lng] = fallbackResources[lng] || {};
25+
fallbackResources[lng][ns] = completeResources[language];
26+
}
27+
}
828
}
929

30+
/**
31+
* This is our i18next "backend" plugin. It tries to fetch languages
32+
* from a CDN.
33+
*/
34+
class FetchResources {
35+
constructor(services, options) {
36+
this.init(services, options);
37+
}
38+
39+
// run fetch with a timeout. Automatically rejects on timeout
40+
// default timeout = 2000 ms
41+
fetchWithTimeout(url, options, timeout = 2000) {
42+
return Promise.race([
43+
fetch(url, options),
44+
new Promise((_, reject) =>
45+
setTimeout(() => reject(new Error('timeout')), timeout)
46+
)
47+
]);
48+
}
49+
50+
init(services, options = {}) {
51+
this.services = services;
52+
this.options = options;
53+
}
54+
55+
read(language, namespace, callback) {
56+
const loadPath = this.options.loadPath;
57+
58+
if (language === this.options.fallback) {
59+
// if the default language of the user is the same as our inbuilt fallback,
60+
// there's no need to fetch resources from the cdn. This won't actually
61+
// need to run when we use "partialBundledLanguages" in the init
62+
// function.
63+
callback(null, fallbackResources[language][namespace]);
64+
} else if (languages.includes(language)) {
65+
// The user's language is included in the list of languages
66+
// that we so far added translations for.
67+
68+
const url = this.services.interpolator.interpolate(loadPath, {
69+
lng: language,
70+
ns: namespace
71+
});
72+
this.loadUrl(url, callback);
73+
} else {
74+
// We don't have translations for this language. i18next will use
75+
// the default language instead.
76+
callback('Not found', false);
77+
}
78+
}
79+
80+
loadUrl(url, callback) {
81+
this.fetchWithTimeout(url)
82+
.then(
83+
response => {
84+
const ok = response.ok;
85+
86+
if (!ok) {
87+
// caught in the catch() below
88+
throw new Error(`failed loading ${url}`);
89+
}
90+
return response.json();
91+
},
92+
() => {
93+
// caught in the catch() below
94+
throw new Error(`failed loading ${url}`);
95+
}
96+
)
97+
.then(data => {
98+
return callback(null, data);
99+
})
100+
.catch(callback);
101+
}
102+
}
103+
FetchResources.type = 'backend';
104+
10105
/**
11106
* This is our translation function. Give it a key and
12107
* it will retreive the appropriate string
@@ -18,38 +113,52 @@ if (typeof IS_MINIFIED === 'undefined') {
18113
* @returns {String} message (with values inserted) in the user's browser language
19114
* @private
20115
*/
21-
export let translator = () => {
116+
export let translator = (key, values) => {
22117
console.debug('p5.js translator called before translations were loaded');
23-
return '';
118+
119+
// Certain FES functionality may trigger before translations are downloaded.
120+
// Using "partialBundledLanguages" option during initialization, we can
121+
// still use our fallback language to display messages
122+
i18next.t(key, values); /* i18next-extract-disable-line */
24123
};
25124
// (We'll set this to a real value in the init function below!)
26125

27126
/**
28127
* Set up our translation function, with loaded languages
29128
*/
30-
export const initialize = () =>
31-
new Promise((resolve, reject) => {
32-
i18next
33-
.use(LanguageDetector)
34-
.init({
35-
fallbackLng: 'en',
36-
nestingPrefix: '$tr(',
37-
nestingSuffix: ')',
38-
defaultNS: 'translation',
39-
returnEmptyString: false,
40-
interpolation: {
41-
escapeValue: false
42-
},
43-
detection: {
44-
checkWhitelist: false
45-
},
46-
resources
47-
})
48-
.then(
49-
translateFn => {
50-
translator = translateFn;
51-
resolve();
52-
},
53-
e => reject(`Translations failed to load (${e})`)
54-
);
55-
});
129+
export const initialize = () => {
130+
let i18init = i18next
131+
.use(LanguageDetector)
132+
.use(FetchResources)
133+
.init({
134+
fallbackLng: 'en',
135+
nestingPrefix: '$tr(',
136+
nestingSuffix: ')',
137+
defaultNS: 'translation',
138+
returnEmptyString: false,
139+
interpolation: {
140+
escapeValue: false
141+
},
142+
detection: {
143+
checkWhitelist: false
144+
},
145+
backend: {
146+
fallback: 'en',
147+
loadPath:
148+
'https://cdn.jsdelivr.net/npm/p5/translations/{{lng}}/{{ns}}.json'
149+
},
150+
partialBundledLanguages: true,
151+
resources: fallbackResources
152+
})
153+
.then(
154+
translateFn => {
155+
translator = translateFn;
156+
},
157+
e => console.debug(`Translations failed to load (${e})`)
158+
);
159+
160+
// i18next.init() returns a promise that resolves when the translations
161+
// are loaded. We use this in core/init.js to hold p5 initialization until
162+
// we have the translation files.
163+
return i18init;
164+
};

tasks/build/browserify.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ module.exports = function(grunt) {
1919
function(param) {
2020
const isMin = param === 'min';
2121
const isTest = param === 'test';
22+
const isDev = param === 'dev';
23+
2224
const filename = isMin
2325
? 'p5.pre-min.js'
2426
: isTest ? 'p5-test.js' : 'p5.js';
@@ -32,16 +34,21 @@ module.exports = function(grunt) {
3234
// Render the banner for the top of the file
3335
const banner = grunt.template.process(bannerTemplate);
3436

37+
let globalVars = {};
38+
if (isDev) {
39+
globalVars['P5_DEV_BUILD'] = () => true;
40+
}
3541
// Invoke Browserify programatically to bundle the code
36-
let browseified = browserify(srcFilePath, {
37-
standalone: 'p5'
42+
let browserified = browserify(srcFilePath, {
43+
standalone: 'p5',
44+
insertGlobalVars: globalVars
3845
});
3946

4047
if (isMin) {
4148
// These paths should be the exact same as what are used in the import
4249
// statements in the source. They are not relative to this file. It's
4350
// just how browserify works apparently.
44-
browseified = browseified
51+
browserified = browserified
4552
.exclude('../../docs/reference/data.json')
4653
.exclude('../../../docs/parameterData.json')
4754
.exclude('../../translations')
@@ -50,13 +57,17 @@ module.exports = function(grunt) {
5057
.ignore('i18next-browser-languagedetector');
5158
}
5259

60+
if (!isDev) {
61+
browserified = browserified.exclude('../../translations/dev');
62+
}
63+
5364
const babelifyOpts = { plugins: ['static-fs'] };
5465

5566
if (isTest) {
5667
babelifyOpts.envName = 'test';
5768
}
5869

59-
const bundle = browseified.transform('babelify', babelifyOpts).bundle();
70+
const bundle = browserified.transform('babelify', babelifyOpts).bundle();
6071

6172
// Start the generated output with the banner comment,
6273
let code = banner + '\n';

translations/dev.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export { default as en_translation } from './en/translation';
2+
export { default as es_translation } from './es/translation';
3+
4+
/**
5+
* When adding a new language, add a new "export" statement above this.
6+
* For example, if we were to add fr ( French ), we would write:
7+
* export { default as fr_translation } from './fr/translation';
8+
*
9+
* If the language key has a hypen (-), replace it with an underscore ( _ )
10+
* e.g. for es-MX we would write:
11+
* export { default as es_MX_translation } from './es-MX/translation';
12+
*
13+
* "es_MX" is the language key whereas "translation" is the filename
14+
* ( translation.json ) or the namespace
15+
*/

translations/index.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
import en from './en/translation';
2-
import es from './es/translation';
2+
3+
// Only one language is imported above. This is intentional as other languages
4+
// will be hosted online and then downloaded whenever needed
35

46
/**
5-
* Maps our translations to their language key
6-
* (`en` is english, `es` es español)
7-
*
8-
* `translation` is the namespace we're using for
9-
* our initial set of translation strings.
7+
* Here, we define a default/fallback language which we can use without internet.
8+
* You won't have to change this when adding a new language.
9+
*
10+
* `translation` is the namespace we are using for our initial set of strings
1011
*/
1112
export default {
1213
en: {
1314
translation: en
14-
},
15-
es: {
16-
translation: es
1715
}
1816
};
17+
18+
/**
19+
* This is a list of languages that we have added so far.
20+
* If you have just added a new language (yay!), add its key to the list below
21+
* (`en` is english, `es` es español). Also add its export to
22+
* dev.js, which is another file in this folder.
23+
*/
24+
export const languages = [
25+
'en',
26+
'es'
27+
];

0 commit comments

Comments
 (0)