diff --git a/app/desktop/webpack.renderer.mjs b/app/desktop/webpack.renderer.mjs index 3c0203d51..62c3cb4a2 100644 --- a/app/desktop/webpack.renderer.mjs +++ b/app/desktop/webpack.renderer.mjs @@ -12,6 +12,11 @@ const require = createRequire(import.meta.url); const IS_PRODUCTION = process.env.NODE_ENV === 'production'; const {version} = require('../../package.json'); +const DEFAULT_TRANSLATE_SERVICE_URL = process.env.TRANSLATE_SERVICE_URL || + 'https://trampoline.simonshiki.top/translate/translate'; +const DEFAULT_TTS_SERVICE_URL = process.env.TTS_SERVICE_URL || + 'https://trampoline.simonshiki.top/tts/synth'; + /** * @typedef {import('webpack-dev-server').Configuration} ConfigurationWithDevServer */ @@ -174,8 +179,10 @@ const rendererConfig = { new webpack.DefinePlugin({ 'process.env.DEBUG': Boolean(process.env.DEBUG), 'process.env.GA_ID': `"${process.env.GA_ID || 'UA-000000-01'}"`, - 'clipcc.VERSION': version, - 'clipcc.BUILD_TIME': Date.now() + 'clipcc.VERSION': JSON.stringify(version), + 'clipcc.BUILD_TIME': Date.now(), + 'clipcc.DEFAULT_TRANSLATE_SERVICE_URL': JSON.stringify(DEFAULT_TRANSLATE_SERVICE_URL), + 'clipcc.DEFAULT_TTS_SERVICE_URL': JSON.stringify(DEFAULT_TTS_SERVICE_URL) }), new NodePolyfillPlugin({ includeAliases: ['buffer', 'Buffer', 'events'] diff --git a/packages/gui/jest.config.js b/packages/gui/jest.config.js index 0f4754402..135ac913c 100644 --- a/packages/gui/jest.config.js +++ b/packages/gui/jest.config.js @@ -5,6 +5,7 @@ module.exports = { testEnvironment: 'jsdom', setupFiles: [ 'raf/polyfill', + '/test/helpers/clipcc-globals.js', '/test/helpers/enzyme-setup.js' ], testPathIgnorePatterns: [ diff --git a/packages/gui/src/components/settings-modal/settings-modal.css b/packages/gui/src/components/settings-modal/settings-modal.css index b75e9830e..082a568fa 100644 --- a/packages/gui/src/components/settings-modal/settings-modal.css +++ b/packages/gui/src/components/settings-modal/settings-modal.css @@ -95,6 +95,10 @@ width: 5rem; } +.input-wide { + width: 18rem; +} + .input::-webkit-outer-spin-button, .input::-webkit-inner-spin-button { appearance: none; margin: 0; diff --git a/packages/gui/src/components/settings-modal/settings-modal.jsx b/packages/gui/src/components/settings-modal/settings-modal.jsx index cf8684c00..4b8ea39dc 100644 --- a/packages/gui/src/components/settings-modal/settings-modal.jsx +++ b/packages/gui/src/components/settings-modal/settings-modal.jsx @@ -32,6 +32,11 @@ const messages = defineMessages({ description: 'Label of project', id: 'gui.settingsModal.project' }, + network: { + defaultMessage: 'Network', + description: 'Label of network', + id: 'gui.settingsModal.network' + }, system: { defaultMessage: 'Follow System', description: 'Label of follow system', @@ -62,7 +67,8 @@ class SettingsModal extends React.Component { this.categoryRef = { appearance: null, // @todo use React.createRef instead after React 16.3 player: null, - project: null + project: null, + network: null }; bindAll(this, [ 'handleJumpToCategoryWrapper' @@ -97,6 +103,9 @@ class SettingsModal extends React.Component {

{this.props.intl.formatMessage(messages.project)}

+

+ {this.props.intl.formatMessage(messages.network)} +

+

this.categoryRef.network = ref} + > + {this.props.intl.formatMessage(messages.network)} +

+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
@@ -413,6 +503,9 @@ SettingsModal.propTypes = { framerate: PropTypes.number.isRequired, stageWidth: PropTypes.number.isRequired, stageHeight: PropTypes.number.isRequired, + translateServiceUrl: PropTypes.string.isRequired, + ttsServiceUrl: PropTypes.string.isRequired, + useScratchOfficialApi: PropTypes.bool.isRequired, theme: PropTypes.string.isRequired, intl: intlShape.isRequired, onClose: PropTypes.func.isRequired, @@ -421,6 +514,9 @@ SettingsModal.propTypes = { onChangeEdgelessStage: PropTypes.func.isRequired, onChangeStageWidth: PropTypes.func.isRequired, onChangeStageHeight: PropTypes.func.isRequired, + onChangeTranslateServiceUrl: PropTypes.func.isRequired, + onChangeTTSServiceUrl: PropTypes.func.isRequired, + onChangeUseScratchOfficialApi: PropTypes.func.isRequired, onChangeUnlimitedListLength: PropTypes.func.isRequired, onChangeUnlimitedPenSize: PropTypes.func.isRequired, onChangeUnlimitedSoundStuffs: PropTypes.func.isRequired, diff --git a/packages/gui/src/containers/settings-modal.jsx b/packages/gui/src/containers/settings-modal.jsx index f27d1d322..bb35908fb 100644 --- a/packages/gui/src/containers/settings-modal.jsx +++ b/packages/gui/src/containers/settings-modal.jsx @@ -29,7 +29,10 @@ class SettingsModal extends React.Component { 'handleChangeAccurateCoordinates', 'handleChangeHideNonVanillaBlocks', 'handleChangeStageWidth', - 'handleChangeStageHeight' + 'handleChangeStageHeight', + 'handleChangeTranslateServiceUrl', + 'handleChangeTTSServiceUrl', + 'handleChangeUseScratchOfficialApi' ]); } handleClose () { @@ -82,6 +85,15 @@ class SettingsModal extends React.Component { handleChangeStageHeight (height) { this.props.updateSettings({stageHeight: Math.round(height)}); } + handleChangeTranslateServiceUrl (url) { + this.props.updateSettings({translateServiceUrl: url.trim().replace(/\/+$/, '')}); + } + handleChangeTTSServiceUrl (url) { + this.props.updateSettings({ttsServiceUrl: url.trim().replace(/\/+$/, '')}); + } + handleChangeUseScratchOfficialApi (value) { + this.props.updateSettings({useScratchOfficialApi: value}); + } render () { return ( ); } @@ -132,7 +150,10 @@ SettingsModal.propTypes = { onClose: PropTypes.func.isRequired, updateSettings: PropTypes.func.isRequired, stageHeight: PropTypes.number.isRequired, - stageWidth: PropTypes.number.isRequired + stageWidth: PropTypes.number.isRequired, + translateServiceUrl: PropTypes.string.isRequired, + ttsServiceUrl: PropTypes.string.isRequired, + useScratchOfficialApi: PropTypes.bool.isRequired }; const mapStateToProps = state => ({ @@ -148,7 +169,10 @@ const mapStateToProps = state => ({ framerate: state.scratchGui.settings.framerate, theme: state.scratchGui.settings.theme, stageHeight: state.scratchGui.settings.stageHeight, - stageWidth: state.scratchGui.settings.stageWidth + stageWidth: state.scratchGui.settings.stageWidth, + translateServiceUrl: state.scratchGui.settings.translateServiceUrl, + ttsServiceUrl: state.scratchGui.settings.ttsServiceUrl, + useScratchOfficialApi: state.scratchGui.settings.useScratchOfficialApi }); const mapDispatchToProps = dispatch => ({ diff --git a/packages/gui/src/lib/vm-manager-hoc.jsx b/packages/gui/src/lib/vm-manager-hoc.jsx index 376904f6e..378b63198 100644 --- a/packages/gui/src/lib/vm-manager-hoc.jsx +++ b/packages/gui/src/lib/vm-manager-hoc.jsx @@ -14,6 +14,9 @@ import { projectError } from '../reducers/project-state'; +const OFFICIAL_TRANSLATE_SERVICE_URL = 'https://translate-service.scratch.mit.edu/translate'; +const OFFICIAL_TTS_SERVICE_URL = 'https://synthesis-service.scratch.mit.edu/synth'; + /** * Higher Order Component to manage events emitted by the VM * @param {React.Component} WrappedComponent component to manage VM events for @@ -46,6 +49,10 @@ const vmManagerHOC = function (WrappedComponent) { }); this.props.vm.setStageWidth(this.props.stageWidth); this.props.vm.setStageHeight(this.props.stageHeight); + if (!this.props.useScratchOfficialApi) { + this.props.vm.setTranslateServiceUrl(this.props.translateServiceUrl); + this.props.vm.setTTSServiceUrl(this.props.ttsServiceUrl); + } } if (!this.props.isPlayerOnly && !this.props.isStarted) { this.props.vm.start(); @@ -102,6 +109,23 @@ const vmManagerHOC = function (WrappedComponent) { if (this.props.stageHeight !== prevProps.stageHeight) { this.props.vm.setStageHeight(this.props.stageHeight); } + if (this.props.useScratchOfficialApi !== prevProps.useScratchOfficialApi) { + if (this.props.useScratchOfficialApi) { + this.props.vm.setTranslateServiceUrl(OFFICIAL_TRANSLATE_SERVICE_URL); + this.props.vm.setTTSServiceUrl(OFFICIAL_TTS_SERVICE_URL); + } else { + this.props.vm.setTranslateServiceUrl(this.props.translateServiceUrl); + this.props.vm.setTTSServiceUrl(this.props.ttsServiceUrl); + } + } + if (!this.props.useScratchOfficialApi) { + if (this.props.translateServiceUrl !== prevProps.translateServiceUrl) { + this.props.vm.setTranslateServiceUrl(this.props.translateServiceUrl); + } + if (this.props.ttsServiceUrl !== prevProps.ttsServiceUrl) { + this.props.vm.setTTSServiceUrl(this.props.ttsServiceUrl); + } + } } loadProject () { return this.props.vm.loadProject(this.props.projectData) @@ -145,6 +169,11 @@ const vmManagerHOC = function (WrappedComponent) { unlimitedPenSize, unlimitedSoundStuffs, accurateCoordinates, + stageWidth, + stageHeight, + translateServiceUrl, + ttsServiceUrl, + useScratchOfficialApi, /* eslint-enable no-unused-vars */ isLoadingWithId: isLoadingWithIdProp, vm, @@ -185,6 +214,9 @@ const vmManagerHOC = function (WrappedComponent) { accurateCoordinates: PropTypes.bool.isRequired, stageWidth: PropTypes.number.isRequired, stageHeight: PropTypes.number.isRequired, + translateServiceUrl: PropTypes.string.isRequired, + ttsServiceUrl: PropTypes.string.isRequired, + useScratchOfficialApi: PropTypes.bool.isRequired, vm: PropTypes.instanceOf(VM).isRequired }; @@ -208,7 +240,10 @@ const vmManagerHOC = function (WrappedComponent) { unlimitedSoundStuffs: state.scratchGui.settings.unlimitedSoundStuffs, accurateCoordinates: state.scratchGui.settings.accurateCoordinates, stageWidth: state.scratchGui.settings.stageWidth, - stageHeight: state.scratchGui.settings.stageHeight + stageHeight: state.scratchGui.settings.stageHeight, + translateServiceUrl: state.scratchGui.settings.translateServiceUrl, + ttsServiceUrl: state.scratchGui.settings.ttsServiceUrl, + useScratchOfficialApi: state.scratchGui.settings.useScratchOfficialApi }; }; diff --git a/packages/gui/src/reducers/settings.ts b/packages/gui/src/reducers/settings.ts index c5de5922f..74e1dff0f 100644 --- a/packages/gui/src/reducers/settings.ts +++ b/packages/gui/src/reducers/settings.ts @@ -18,6 +18,9 @@ export type SettingsState = { theme: string; stageWidth: number; stageHeight: number; + translateServiceUrl: string; + ttsServiceUrl: string; + useScratchOfficialApi: boolean; }; const defaultState: SettingsState = { @@ -34,7 +37,10 @@ const defaultState: SettingsState = { framerate: 30, theme: 'system', stageWidth: 480, - stageHeight: 360 + stageHeight: 360, + translateServiceUrl: clipcc.DEFAULT_TRANSLATE_SERVICE_URL, + ttsServiceUrl: clipcc.DEFAULT_TTS_SERVICE_URL, + useScratchOfficialApi: false }; const parseSavedSettings = (): Partial => { @@ -49,24 +55,14 @@ const parseSavedSettings = (): Partial => { return {}; } - return { - hideNonVanillaBlocks: - typeof parsed.hideNonVanillaBlocks === 'boolean' ? parsed.hideNonVanillaBlocks : undefined, - autoSave: typeof parsed.autoSave === 'boolean' ? parsed.autoSave : undefined, - infiniteCloning: typeof parsed.infiniteCloning === 'boolean' ? parsed.infiniteCloning : undefined, - edgelessStage: typeof parsed.edgelessStage === 'boolean' ? parsed.edgelessStage : undefined, - unlimitedListLength: typeof parsed.unlimitedListLength === 'boolean' ? parsed.unlimitedListLength : undefined, - unlimitedPenSize: typeof parsed.unlimitedPenSize === 'boolean' ? parsed.unlimitedPenSize : undefined, - unlimitedSoundStuffs: - typeof parsed.unlimitedSoundStuffs === 'boolean' ? parsed.unlimitedSoundStuffs : undefined, - accurateCoordinates: typeof parsed.accurateCoordinates === 'boolean' ? parsed.accurateCoordinates : undefined, - autoSaveInterval: typeof parsed.autoSaveInterval === 'number' ? parsed.autoSaveInterval : undefined, - compression: typeof parsed.compression === 'number' ? parsed.compression : undefined, - framerate: typeof parsed.framerate === 'number' ? parsed.framerate : undefined, - theme: typeof parsed.theme === 'string' ? parsed.theme : undefined, - stageWidth: typeof parsed.stageWidth === 'number' ? parsed.stageWidth : undefined, - stageHeight: typeof parsed.stageHeight === 'number' ? parsed.stageHeight : undefined - }; + const result: Partial = {}; + for (const key of Object.keys(defaultState) as Array) { + const value = parsed[key]; + if (value !== undefined && typeof value === typeof defaultState[key]) { + (result as Record)[key] = value; + } + } + return result; }; const initialState: SettingsState = { diff --git a/packages/gui/src/types.d.ts b/packages/gui/src/types.d.ts index a239de4cb..203a74401 100644 --- a/packages/gui/src/types.d.ts +++ b/packages/gui/src/types.d.ts @@ -1,3 +1,10 @@ +declare const clipcc: { + VERSION?: string; + BUILD_TIME?: number; + DEFAULT_TRANSLATE_SERVICE_URL: string; + DEFAULT_TTS_SERVICE_URL: string; +}; + declare module '!!arraybuffer-loader!.*' { declare const value: ArrayBuffer; export default value; diff --git a/packages/gui/test/helpers/clipcc-globals.js b/packages/gui/test/helpers/clipcc-globals.js new file mode 100644 index 000000000..f3c8b9110 --- /dev/null +++ b/packages/gui/test/helpers/clipcc-globals.js @@ -0,0 +1,5 @@ +global.clipcc = { + DEFAULT_TRANSLATE_SERVICE_URL: + process.env.TRANSLATE_SERVICE_URL ?? 'https://translate-service.scratch.mit.edu/translate', + DEFAULT_TTS_SERVICE_URL: process.env.TTS_SERVICE_URL ?? 'https://synthesis-service.scratch.mit.edu/synth' +}; diff --git a/packages/gui/test/unit/util/vm-manager-hoc.test.jsx b/packages/gui/test/unit/util/vm-manager-hoc.test.jsx index b92065ced..3f4ab9cec 100644 --- a/packages/gui/test/unit/util/vm-manager-hoc.test.jsx +++ b/packages/gui/test/unit/util/vm-manager-hoc.test.jsx @@ -35,6 +35,8 @@ describe('VMManagerHOC', () => { vm.attachAudioEngine = jest.fn(); vm.setCompatibilityMode = jest.fn(); vm.setLocale = jest.fn(); + vm.setTranslateServiceUrl = jest.fn(); + vm.setTTSServiceUrl = jest.fn(); vm.start = jest.fn(); }); test('when it mounts in player mode, the vm is initialized but not started', () => { diff --git a/packages/gui/webpack.config.js b/packages/gui/webpack.config.js index efbf6d5b2..3fa601cf9 100644 --- a/packages/gui/webpack.config.js +++ b/packages/gui/webpack.config.js @@ -16,6 +16,11 @@ const BUILD_DIST = IS_PRODUCTION || process.env.BUILD_MODE === 'dist'; const IS_CI = process.env.CI; const LIBRARY_ONLY = process.env.LIBRARY_ONLY; +const DEFAULT_TRANSLATE_SERVICE_URL = process.env.TRANSLATE_SERVICE_URL || + 'https://translate-service.scratch.mit.edu/translate'; +const DEFAULT_TTS_SERVICE_URL = process.env.TTS_SERVICE_URL || + 'https://synthesis-service.scratch.mit.edu/synth'; + const base = { mode: IS_PRODUCTION ? 'production' : 'development', devtool: 'cheap-module-source-map', @@ -110,6 +115,12 @@ const base = { ] }), new NodePolyfillPlugin(), + new webpack.DefinePlugin({ + 'clipcc.DEFAULT_TRANSLATE_SERVICE_URL': JSON.stringify(DEFAULT_TRANSLATE_SERVICE_URL), + 'clipcc.DEFAULT_TTS_SERVICE_URL': JSON.stringify(DEFAULT_TTS_SERVICE_URL), + 'clipcc.VERSION': JSON.stringify(version), + 'clipcc.BUILD_TIME': Date.now() + }), new CopyWebpackPlugin({ patterns: [ { @@ -181,9 +192,7 @@ module.exports = (BUILD_DIST ? [] : [ plugins: base.plugins.concat([ new webpack.DefinePlugin({ 'process.env.DEBUG': Boolean(process.env.DEBUG), - 'process.env.GA_ID': `"${process.env.GA_ID || 'UA-000000-01'}"`, - 'clipcc.VERSION': version, - 'clipcc.BUILD_TIME': Date.now() + 'process.env.GA_ID': `"${process.env.GA_ID || 'UA-000000-01'}"` }), new HtmlWebpackPlugin({ chunks: ['lib.min', 'gui'], diff --git a/packages/vm/jest.config.js b/packages/vm/jest.config.js index 386a3af16..fc65cf2d0 100644 --- a/packages/vm/jest.config.js +++ b/packages/vm/jest.config.js @@ -1,6 +1,9 @@ /** @satisfies {import('jest').Config} */ module.exports = { testEnvironment: 'node', + setupFiles: [ + '/test/helpers/clipcc-globals.js' + ], testMatch: [ '/test/unit/*.js', '/test/integration/*.js' diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index 8eb4a648d..7074eed9d 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -538,6 +538,20 @@ class Runtime extends EventEmitter { unlimitedSoundStuffs: false }; + /** + * The url of the translate service used by the translate extension. + * @type {string} + */ + // eslint-disable-next-line no-undef + this.translateServiceUrl = clipcc.DEFAULT_TRANSLATE_SERVICE_URL; + + /** + * The url of the text-to-speech service used by the text2speech extension. + * @type {string} + */ + // eslint-disable-next-line no-undef + this.ttsServiceUrl = clipcc.DEFAULT_TTS_SERVICE_URL; + /** * A reference to the current runtime stepping interval, set * by a `setInterval`. diff --git a/packages/vm/src/extensions/scratch3_text2speech/index.js b/packages/vm/src/extensions/scratch3_text2speech/index.js index 0755d0199..88ffd0d0c 100644 --- a/packages/vm/src/extensions/scratch3_text2speech/index.js +++ b/packages/vm/src/extensions/scratch3_text2speech/index.js @@ -22,12 +22,6 @@ const menuIconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZ // eslint-disable-next-line max-len const blockIconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNDBweCIgaGVpZ2h0PSI0MHB4IiB2aWV3Qm94PSIwIDAgNDAgNDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjIgKDY3MTQ1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5FeHRlbnNpb25zL1NvZnR3YXJlL1RleHQtdG8tU3BlZWNoLUJsb2NrPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGcgaWQ9IkV4dGVuc2lvbnMvU29mdHdhcmUvVGV4dC10by1TcGVlY2gtQmxvY2siIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS1vcGFjaXR5PSIwLjE1Ij4KICAgICAgICA8ZyBpZD0idGV4dDJzcGVlY2giIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQuMDAwMDAwLCA0LjAwMDAwMCkiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjMDAwMDAwIj4KICAgICAgICAgICAgPHBhdGggZD0iTTExLjUsMTcuNjY5MzQzNSBDMTEuNSwxNi42NTM5MjY5IDEwLjAwNjAxNDUsMTYuMDg0NDI3NCA5LjExMjU2MDI0LDE2Ljg4ODMgTDYuNDEyNTYwMjQsMTkuMDUwNzE0IEM1LjM5MzQ2NzU1LDE5Ljg2Njg5OTQgNC4wNzQ5NzM1MSwyMC4zMzE3NTc1IDIuNywyMC4zMzE3NTc1IEwyLjMsMjAuMzMxNzU3NSBDMS4yNjUxOTIzMywyMC4zMzE3NTc1IDAuNSwyMS4wMjEyMDAzIDAuNSwyMS45MDQwNzEgTDAuNSwyNi4xMzg3OTg2IEMwLjUsMjcuMDIxNjY5MyAxLjI2NTE5MjMzLDI3LjcxMTExMiAyLjMsMjcuNzExMTEyIEwyLjcsMjcuNzExMTEyIEM0LjE1NzU1NjgyLDI3LjcxMTExMiA1LjQ1MzcyMzIyLDI4LjEzMzUyNzEgNi41MTk3MjA5OCwyOC45OTggTDkuMTE4NDAyOTMsMzEuMTU5MzIxNiBDMTAuMDI2MTg1NSwzMS45MDkwNzkzIDExLjUsMzEuMzQ3MjY4OSAxMS41LDMwLjI4MzQyNTUgTDExLjUsMTcuNjY5MzQzNSBaIiBpZD0ic3BlYWtlciIgZmlsbD0iIzRENEQ0RCI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNMjEuNjQzNjA2NiwxNi41IEMxOS45NzcwMDk5LDE4LjQzNzAyMzQgMTcuMTA1MDI3NSwxOS45Mjg1NzE0IDE1LjY2NjY2NjcsMTkuOTI4NTcxNCBDMTUuNTEyNjM5NywxOS45Mjg1NzE0IDE1LjMxNjYyOTIsMTkuODk1OTAzIDE1LjEwOTcyNjUsMTkuNzkyNDUxNyBDMTQuNzM3NjAzOSwxOS42MDYzOTA0IDE0LjUsMTkuMjQ5OTg0NiAxNC41LDE4Ljc2MTkwNDggQzE0LjUsMTguNjU2ODA0MSAxNC41MTcwNTU1LDE4LjU1NDUwNzYgMTQuNTQ5NDQ2NywxOC40NTQwODQ0IEMxNC42MjU3NTQ1LDE4LjIxNzUwNjMgMTUuMTczNTcyMSwxNy40Njc1MzEgMTUuMjc3MjA3MSwxNy4yODA5ODgxIEMxNS41NDYzNTI2LDE2Ljc5NjUyNjEgMTUuNzM5MDI1LDE2LjIwNjM1NjEgMTUuODQzMjg5MSwxNS40MTYwMDM0IEMxMy4xODk3MDA1LDEzLjkyNjgzNjkgMTEuNSwxMS4xMTM5NjY4IDExLjUsOCBDMTEuNSwzLjMwNTU3OTYzIDE1LjMwNTU3OTYsLTAuNSAyMCwtMC41IEwyNCwtMC41IEMyOC42OTQ0MjA0LC0wLjUgMzIuNSwzLjMwNTU3OTYzIDMyLjUsOCBDMzIuNSwxMi42OTQ0MjA0IDI4LjY5NDQyMDQsMTYuNSAyNCwxNi41IEwyMS42NDM2MDY2LDE2LjUgWiIgaWQ9InNwZWVjaCIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+'; -/** - * The url of the synthesis server. - * @type {string} - */ -const SERVER_HOST = 'https://synthesis-service.scratch.mit.edu'; - /** * Whether supports Web Speech API. * Since original Web Speech API is unavailable im Mainland China, @@ -725,7 +719,7 @@ class Scratch3Text2SpeechBlocks { } // Build up URL - let path = `${SERVER_HOST}/synth`; + let path = `${this.runtime.ttsServiceUrl}`; path += `?locale=${locale}`; path += `&gender=${gender}`; path += `&text=${encodeURIComponent(words.substring(0, 128))}`; diff --git a/packages/vm/src/extensions/scratch3_translate/index.js b/packages/vm/src/extensions/scratch3_translate/index.js index 9840625f4..174602cb6 100644 --- a/packages/vm/src/extensions/scratch3_translate/index.js +++ b/packages/vm/src/extensions/scratch3_translate/index.js @@ -20,12 +20,6 @@ const menuIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAA // eslint-disable-next-line max-len const blockIconURI = 'data:image/png;base64,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'; -/** - * The url of the translate server. - * @type {string} - */ -const serverURL = 'https://translate-service.scratch.mit.edu/'; - /** * How long to wait in ms before timing out requests to translate server. * @type {int} @@ -37,7 +31,13 @@ const serverTimeoutMs = 10000; // 10 seconds (chosen arbitrarily). * @class */ class Scratch3TranslateBlocks { - constructor () { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + /** * Language code of the viewer, based on their locale. * @type {string} @@ -259,7 +259,7 @@ class Scratch3TranslateBlocks { const lang = this.getLanguageCodeFromArg(args.LANGUAGE); - let urlBase = `${serverURL}translate?language=`; + let urlBase = `${this.runtime.translateServiceUrl}?language=`; urlBase += lang; urlBase += '&text='; urlBase += encodeURIComponent(args.WORDS); diff --git a/packages/vm/src/types/global.d.ts b/packages/vm/src/types/global.d.ts index 0ab8d9639..d130ac1c7 100644 --- a/packages/vm/src/types/global.d.ts +++ b/packages/vm/src/types/global.d.ts @@ -1,3 +1,10 @@ declare global { type int = number; + + const clipcc: { + VERSION?: string; + BUILD_TIME?: number; + DEFAULT_TRANSLATE_SERVICE_URL: string; + DEFAULT_TTS_SERVICE_URL: string; + }; } diff --git a/packages/vm/src/virtual-machine.js b/packages/vm/src/virtual-machine.js index b0e03b4f4..d557e1384 100644 --- a/packages/vm/src/virtual-machine.js +++ b/packages/vm/src/virtual-machine.js @@ -264,6 +264,22 @@ class VirtualMachine extends EventEmitter { } } + /** + * Set the url of the translate service used by the translate extension. + * @param {string} url The full url of the translate endpoint, without query string. + */ + setTranslateServiceUrl (url) { + this.runtime.translateServiceUrl = url; + } + + /** + * Set the url of the text-to-speech service used by the text2speech extension. + * @param {string} url The full url of the text-to-speech endpoint, without query string. + */ + setTTSServiceUrl (url) { + this.runtime.ttsServiceUrl = url; + } + /** * Set stage size. * @param {number} width Width of the stage in pixels. diff --git a/packages/vm/test/helpers/clipcc-globals.js b/packages/vm/test/helpers/clipcc-globals.js new file mode 100644 index 000000000..f3c8b9110 --- /dev/null +++ b/packages/vm/test/helpers/clipcc-globals.js @@ -0,0 +1,5 @@ +global.clipcc = { + DEFAULT_TRANSLATE_SERVICE_URL: + process.env.TRANSLATE_SERVICE_URL ?? 'https://translate-service.scratch.mit.edu/translate', + DEFAULT_TTS_SERVICE_URL: process.env.TTS_SERVICE_URL ?? 'https://synthesis-service.scratch.mit.edu/synth' +}; diff --git a/packages/vm/webpack.config.js b/packages/vm/webpack.config.js index c5c9e2f89..0a8825d6d 100644 --- a/packages/vm/webpack.config.js +++ b/packages/vm/webpack.config.js @@ -7,6 +7,11 @@ const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); const RuleInheritancePlugin = require('rule-inheritance-webpack-plugin'); const {version} = require('../../package.json'); +const DEFAULT_TRANSLATE_SERVICE_URL = process.env.TRANSLATE_SERVICE_URL || + 'https://translate-service.scratch.mit.edu/translate'; +const DEFAULT_TTS_SERVICE_URL = process.env.TTS_SERVICE_URL || + 'https://synthesis-service.scratch.mit.edu/synth'; + const base = { mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', devtool: 'cheap-module-source-map', @@ -58,7 +63,9 @@ const base = { new NodePolyfillPlugin(), new webpack.DefinePlugin({ 'clipcc.VERSION': version, - 'clipcc.BUILD_TIME': Date.now() + 'clipcc.BUILD_TIME': Date.now(), + 'clipcc.DEFAULT_TRANSLATE_SERVICE_URL': JSON.stringify(DEFAULT_TRANSLATE_SERVICE_URL), + 'clipcc.DEFAULT_TTS_SERVICE_URL': JSON.stringify(DEFAULT_TTS_SERVICE_URL) }), new webpack.IgnorePlugin({ resourceRegExp: /canvas/,