Extension L10N - VM part (#133)

Sandboxed extensions will use navigator.language
Unsandboxed extensions will use the language set by setLocale
This commit is contained in:
GarboMuffin authored and GitHub committed 2023-05-18 00:00:59 -05:00
1 parent 8a7ae3f774
commit 398892c6f2
6 files changed
+165 -1

No files matched your search

+5 -1
View File
@@ -4,6 +4,9 @@ const ScratchCommon = require('./tw-extension-api-common');
const dispatch = require('../dispatch/worker-dispatch');
const log = require('../util/log');
const {isWorker} = require('./tw-extension-worker-context');
const createTranslate = require('./tw-l10n');
const translate = createTranslate(null);
const loadScripts = url => {
if (isWorker) {
@@ -75,7 +78,8 @@ Object.assign(global.Scratch, ScratchCommon, {
canOpenWindow: () => Promise.resolve(false),
openWindow: () => Promise.reject(new Error('Scratch.openWindow not supported in sandboxed extensions')),
canRedirect: () => Promise.resolve(false),
redirect: () => Promise.reject(new Error('Scratch.redirect not supported in sandboxed extensions'))
redirect: () => Promise.reject(new Error('Scratch.redirect not supported in sandboxed extensions')),
translate
});
/**
+51
View File
@@ -0,0 +1,51 @@
const formatMessage = require('format-message');
/**
* @param {VM|null} vm
* @returns {object}
*/
const createTranslate = vm => {
const namespace = formatMessage.namespace();
const translate = (message, args) => {
if (message && typeof message === 'object') {
// already in the expected format
} else if (typeof message === 'string') {
message = {
default: message
};
} else {
throw new Error('unsupported data type in translate()');
}
return namespace(message, args);
};
const generateId = defaultMessage => `_${defaultMessage}`;
const getLocale = () => {
if (vm) return vm.getLocale();
if (typeof navigator !== 'undefined') return navigator.language;
return 'en';
};
translate.setup = translations => {
namespace.setup({
locale: getLocale(),
missingTranslation: 'ignore',
generateId,
translations
});
};
translate.setup({});
if (vm) {
vm.on('LOCALE_CHANGED', () => {
translate.setup({});
});
}
return translate;
};
module.exports = createTranslate;
@@ -1,5 +1,6 @@
const ScratchCommon = require('./tw-extension-api-common');
const AsyncLimiter = require('../util/async-limiter');
const createTranslate = require('./tw-l10n');
/**
* Parse a URL object or return null.
@@ -98,6 +99,8 @@ const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => {
location.href = url;
};
Scratch.translate = createTranslate(vm);
global.Scratch = Scratch;
global.ScratchExtensions = require('./tw-scratchx-compatibility-layer');
});
+1
View File
@@ -1354,6 +1354,7 @@ class VirtualMachine extends EventEmitter {
if (locale !== formatMessage.setup().locale) {
formatMessage.setup({locale: locale, translations: {[locale]: messages}});
}
this.emit('LOCALE_CHANGED', locale);
return this.extensionManager.refreshBlocks();
}
+50
View File
@@ -56,3 +56,53 @@ test('redirect', async t => {
await t.rejects(global.Scratch.redirect('https://example.com/'), /^Scratch\.redirect not supported in sandboxed extensions$/);
t.end();
});
test('translate', t => {
t.equal(global.Scratch.translate({
id: 'test1',
default: 'Message 1: {var}',
description: 'Description'
}, {
var: 'test'
}), 'Message 1: test');
t.equal(global.Scratch.translate('test1'), 'test1');
t.equal(global.Scratch.translate('test1 {VAR}', {
VAR: '3'
}), 'test1 3');
const messages = {
en: {
test1: 'EN Message 1: {var}'
},
es: {
test1: 'ES Message 1: {var}'
}
};
// Should default to English when no navigator object
global.Scratch.translate.setup(messages);
t.equal(global.Scratch.translate({
id: 'test1',
default: 'Message 1',
description: 'Description'
}, {
var: 'ok'
}), 'EN Message 1: ok');
// But if there is a navigator object, it should use its language
// This is slightly contrived because setup() should only be run once in real extensions,
// but this should still be useful as a test.
global.navigator = {
language: 'es'
};
global.Scratch.translate.setup(messages);
t.equal(global.Scratch.translate({
id: 'test1',
default: 'Message 1',
description: 'Description'
}, {
var: 'ok'
}), 'ES Message 1: ok');
t.end();
});
+55
View File
@@ -243,3 +243,58 @@ test('redirect', async t => {
t.equal(global.location.href, 'https://example.com/2');
t.end();
});
test('translate', async t => {
const vm = new VirtualMachine();
UnsandboxedExtensionRunner.setupUnsandboxedExtensionAPI(vm);
t.equal(global.Scratch.translate({
id: 'test1',
default: 'Message 1: {var}',
description: 'Description'
}, {
var: 'test'
}), 'Message 1: test');
t.equal(global.Scratch.translate('test1 {var}', {
var: 'ok'
}), 'test1 ok');
global.Scratch.translate.setup({
en: {
test1: 'EN Message 1: {var}'
},
es: {
test1: 'ES Message 1: {var}'
}
});
t.equal(global.Scratch.translate({
id: 'test1',
default: 'Message 1: {var}',
description: 'Description'
}, {
var: 'test'
}), 'EN Message 1: test');
t.equal(global.Scratch.translate('test1 {var}', {
var: 'ok'
}), 'test1 ok');
await vm.setLocale('es');
global.Scratch.translate.setup({
en: {
test1: 'EN Message 1: {var}'
},
es: {
test1: 'ES Message 1: {var}'
}
});
t.equal(global.Scratch.translate({
id: 'test1',
default: 'Message 1: {var}',
description: 'Description'
}, {
var: 'test'
}), 'ES Message 1: test');
t.end();
});