(i18n-js:4) use i18nliner-canvas from npm

refs FOO-2801
flag = none

[change-merged][build-registry-path=jenkins/canvas-lms/foo-2801]

gems/canvas_i18nliner is now a package @instructure/i18nliner-canvas and
lives in the same repo on github along with the 3 other i18nliner
libraries.. this was done to make it easier for maintainers to deal with
this code, since changing one part may break the other due to how
they're architected

the source on github: https://github.com/instructure/i18nliner-js

~ test plan ~

build is still OK, this only affects the generation of files, and those
i manually verified to be identical before and after

Change-Id: I78afa8a808f1699c10aced8466cfade066848bc9
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/294209
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Charley Kline <ckline@instructure.com>
QA-Review: Charley Kline <ckline@instructure.com>
Product-Review: Charley Kline <ckline@instructure.com>
This commit is contained in:
Ahmad Amireh 2022-06-19 13:52:13 +03:00
parent 7a52d3e1ef
commit 14faf088c0
57 changed files with 143 additions and 1430 deletions

View File

@ -1,6 +1,5 @@
/doc/*
/ui-build/**
/gems/canvas_i18nliner/*
/packages/*/node_modules/**
/spec/javascripts/support/jquery.mockjax.js
/spec/selenium/helpers/jquery.simulate.js

View File

@ -1,6 +1,6 @@
{
"plugins": [
"@instructure/i18nliner-handlebars"
"@instructure/i18nliner-canvas"
],
"include": [

View File

@ -74,7 +74,6 @@ RUN set -eux; \
app/stylesheets/brandable_css_brands \
app/views/info \
config/locales/generated \
gems/canvas_i18nliner/node_modules \
log \
node_modules \
packages/canvas-media/es \

View File

@ -42,7 +42,6 @@ RUN --mount=target=/tmp/src \
\
/tmp/dst && \
find \
gems/canvas_i18nliner \
gems/plugins/* \
ui/shared/* \
packages/* \
@ -69,11 +68,6 @@ RUN --mount=target=/tmp/src \
-maxdepth 2 \
-path "gems/*/lib" \
-exec cp -rf --parents {} /tmp/dst \; && \
find gems/canvas_i18nliner \
-not -path "gems/canvas_i18nliner" \
-not -path "gems/canvas_i18nliner/spec" \
-not -path "gems/canvas_i18nliner/spec/*" \
-exec cp -rf --parents {} /tmp/dst \; && \
find gems/plugins \( \
-path "*/app/coffeescripts" -o \
-path "*/app/jsx" -o \

View File

@ -1 +0,0 @@
ember/shared/helpers/t.js

View File

@ -1,12 +0,0 @@
{
"files": [
{
"pattern": "**/*.{coffee,js}",
"processor": "js"
},
{
"pattern": "ember/**/*.hbs",
"processor": "hbs"
}
]
}

View File

@ -1 +0,0 @@
/plugins

View File

@ -18,7 +18,6 @@ services:
- canvas-rce_canvas:/usr/src/app/packages/canvas-rce/canvas
- canvas-rce_lib:/usr/src/app/packages/canvas-rce/lib
- canvas-rce_node_modules:/usr/src/app/packages/canvas-rce/node_modules
- i18nliner_node_modules:/usr/src/app/gems/canvas_i18nliner/node_modules
- jest-moxios-utils_node_modules:/usr/src/app/packages/jest-moxios-utils/node_modules
- js-utils_es:/usr/src/app/packages/js-utils/es
- js-utils_lib:/usr/src/app/packages/js-utils/lib

View File

@ -28,7 +28,6 @@ services:
- canvas-rce_canvas:/usr/src/app/packages/canvas-rce/canvas
- canvas-rce_lib:/usr/src/app/packages/canvas-rce/lib
- canvas-rce_node_modules:/usr/src/app/packages/canvas-rce/node_modules
- i18nliner_node_modules:/usr/src/app/gems/canvas_i18nliner/node_modules
- jest-moxios-utils_node_modules:/usr/src/app/packages/jest-moxios-utils/node_modules
- js-utils_es:/usr/src/app/packages/js-utils/es
- js-utils_lib:/usr/src/app/packages/js-utils/lib

View File

@ -9,7 +9,6 @@ services:
- .:/usr/src/app
- brandable_css_brands:/usr/src/app/stylesheets/brandable_css_brands
- public_dist:/usr/src/app/public/dist
- i18nliner_node_modules:/usr/src/app/gems/canvas_i18nliner/node_modules
- log:/usr/src/app/log
- node_modules:/usr/src/app/node_modules
- tmp:/usr/src/app/tmp

View File

@ -23,7 +23,6 @@ USER docker
RUN set -eux; \
mkdir -p \
app/stylesheets/brandable_css_brands \
gems/canvas_i18nliner/node_modules \
log \
node_modules \
tmp \

View File

@ -1 +0,0 @@
/node_modules

View File

@ -1,5 +0,0 @@
{
"plugins": [
"@instructure/i18nliner-handlebars"
]
}

View File

@ -1,60 +0,0 @@
# canvas_i18nliner
## i18nliner, canvas style
this will replace the i18n_tasks and i18n_extraction gems
`.i18nrc` files must glob for files to be included through the `files` property:
```json
{
"files": [
{
"pattern": "**/*.js",
"processor": "js"
},
{
"pattern": "**/*.{hbs,handlebars}",
"processor": "hbs"
}
]
}
```
`.i18nrc` files can include other directories that may in turn specify more
files through their `.i18nrc` configuration file:
```json
{
"include": [ "relative/path/to/dir" ]
}
```
`.i18nignore` files can exclude files from processing relative to where they
are defined (similar to `.gitignore`):
```json
// file: app/.i18nignore
foo
bar/**/*.js
```
The above ignore file will exclude `app/foo` and `app/bar/**/*.js`.
**Where to place ignore lists?**
The scanner will always look for an `.i18nignore` adjacent to `.i18nrc`, but it
will also discover and use any `.i18nignore` file found between the root
and the target file:
```
app
├── .i18nignore
└── a
├── .i18nignore
└── b
└── c
```
A file under `app/a/b/c/` is subject to exclusion according to rules found in
both `app/.i18nignore` and `app/a/.i18nignore`.

View File

@ -1,3 +0,0 @@
#!/usr/bin/env node
require('../js/main').runCommand(process.argv.slice(2));

View File

@ -1,42 +0,0 @@
#!/usr/bin/env node
var readline = require('readline');
var Handlebars = require('handlebars');
var EmberHandlebars = require('ember-template-compiler').EmberHandlebars;
var ScopedHbsExtractor = require('../js/scoped_hbs_extractor');
var PreProcessor = require('@instructure/i18nliner-handlebars/dist/lib/pre_processor').default;
// make sure necessary overrides are set up (e.g. HbsPreProcessor.normalizeInterpolationKey)
require("../js/main");
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
rl.on('line', function(line) {
var data = JSON.parse(line);
var path = data.path;
var source = data.source;
try {
var translationCount = 0;
var ast = Handlebars.parse(source);
var extractor = new ScopedHbsExtractor(ast, {path: path});
var scope = extractor.scope;
PreProcessor.scope = scope;
PreProcessor.process(ast);
extractor.forEach(function() { translationCount++; });
var precompiler = data.ember ? EmberHandlebars : Handlebars;
var result = precompiler.precompile(ast).toString();
var payload = {template: result, scope: scope, translationCount: translationCount};
process.stdout.write(JSON.stringify(payload) + "\n");
}
catch (e) {
e = e.message || e;
process.stdout.write(JSON.stringify({error: e}) + "\n");
}
});

View File

@ -1,23 +0,0 @@
/*
* Copyright (C) 2022 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const { default: Errors } = require("@instructure/i18nliner/dist/lib/errors");
Errors.register("UnscopedTranslateCall");
module.exports = Errors

View File

@ -1,80 +0,0 @@
/*
* Copyright (C) 2014 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var I18nliner = require("@instructure/i18nliner/dist/lib/main").default;
var TranslateCall = require("@instructure/i18nliner/dist/lib/extractors/translate_call").default;
var Commands = I18nliner.Commands;
var Check = Commands.Check;
var mkdirp = require("mkdirp");
var fs = require("fs");
/*
* GenerateJs determines what needs to go into each i18n js bundle (one
* per "i18n!scope"), based on the I18n.t calls in the code
*
* outputs a json file containing a mapping of scopes <-> translation keys,
* e.g.
*
* {
* "users": [
* "users.title",
* "users.labels.foo",
* "foo_bar_baz" // could be from a different scope, if called within the users scope
* ],
* "groups:" [
* ...
* ],
* ...
*
*/
function GenerateJs(options) {
Check.call(this, options)
}
GenerateJs.prototype = Object.create(Check.prototype);
GenerateJs.prototype.constructor = GenerateJs;
GenerateJs.prototype.run = function() {
var translationsWas = TranslateCall.prototype.translations
TranslateCall.prototype.translations = function() {
var key = this.key;
var defaultValue = this.defaultValue;
if (typeof defaultValue === 'string' || !defaultValue)
return [[key, defaultValue]];
var translations = [];
for (var k in defaultValue) {
if (defaultValue.hasOwnProperty(k)) {
translations.push([key + "." + k, defaultValue[k]]);
}
}
return translations;
};
var success = Check.prototype.run.call(this);
if (!success) return false;
var keysByScope = this.translations.keysByScope();
this.outputFile = './' + (this.options.outputFile || "config/locales/generated/js_bundles.json");
mkdirp.sync(this.outputFile.replace(/\/[^\/]+$/, ''));
fs.writeFileSync(this.outputFile, JSON.stringify(keysByScope));
TranslateCall.prototype.translations = translationsWas
return true;
};
module.exports = GenerateJs;

View File

@ -1,108 +0,0 @@
/*
* Copyright (C) 2014 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var I18nliner = require("@instructure/i18nliner/dist/lib/main").default;
var Commands = I18nliner.Commands;
var Check = Commands.Check;
var CoffeeScript = require("coffee-script");
var babylon = require("@babel/parser");
var fs = require('fs');
var AbstractProcessor = require("@instructure/i18nliner/dist/lib/processors/abstract_processor").default;
var JsProcessor = require("@instructure/i18nliner/dist/lib/processors/js_processor").default;
var HbsProcessor = require("@instructure/i18nliner-handlebars/dist/lib/hbs_processor").default;
var CallHelpers = require("@instructure/i18nliner/dist/lib/call_helpers").default;
var scanner = require("./scanner");
// tell i18nliner's babylon how to handle `import('../foo').then`
I18nliner.config.babylonPlugins.push('dynamicImport')
I18nliner.config.babylonPlugins.push('optionalChaining')
// tell i18nliner's babylon how to handle typescript
I18nliner.config.babylonPlugins.push('typescript')
AbstractProcessor.prototype.checkFiles = function() {
const processor = this.constructor.name.replace(/Processor/, '').toLowerCase()
const files = scanner.getFilesForProcessor(processor)
for (const file of files) {
this.checkWrapper(file, this.checkFile.bind(this))
}
}
JsProcessor.prototype.sourceFor = function(file) {
var source = fs.readFileSync(file).toString();
var data = { source: source, skip: !source.match(/I18n\.t/) };
if (!data.skip) {
if (file.match(/\.coffee$/)) {
data.source = CoffeeScript.compile(source, {});
}
data.ast = babylon.parse(data.source, { plugins: I18nliner.config.babylonPlugins, sourceType: "module" });
}
return data;
};
// we do the actual pre-processing in sourceFor, so just pass data straight through
JsProcessor.prototype.preProcess = function(data) {
return data;
};
require("./scoped_hbs_pre_processor");
var ScopedESMExtractor = require("./scoped_esm_extractor");
var ScopedHbsExtractor = require("./scoped_hbs_extractor");
var ScopedTranslationHash = require("./scoped_translation_hash");
// remove path stuff we don't want in the scope
var pathRegex = new RegExp(
'.*(' +
'ui/shared/jst' +
'|ui/features/screenreader_gradebook/jst' +
'|packages/[^/]+/src/jst' +
'|gems/plugins/[^/]+/app/views/jst' +
')'
)
ScopedHbsExtractor.prototype.normalizePath = function(path) {
return path.replace(pathRegex, "").replace(/^([^\/]+\/)templates\//, '$1');
};
var GenerateJs = require("./generate_js");
Commands.Generate_js = GenerateJs;
// swap out the defaults for our scope-aware varieties
Check.prototype.TranslationHash = ScopedTranslationHash;
JsProcessor.prototype.I18nJsExtractor = ScopedESMExtractor;
HbsProcessor.prototype.Extractor = ScopedHbsExtractor;
CallHelpers.keyPattern = /^\#?\w+(\.\w+)+$/ // handle our absolute keys
module.exports = {
I18nliner,
runCommand: function(argv) {
argv = require('minimist')(argv);
scanner.scanFilesFromI18nrc(
scanner.loadConfigFromDirectory(
require('path').resolve(__dirname, '../../..')
)
)
Commands.run(argv._[0], argv) || (process.exitCode = 1);
}
};

View File

@ -1,81 +0,0 @@
const path = require('path')
const glob = require('glob')
const fs = require('fs')
const filesByProcessor = { js: [], hbs: [], ts: [] }
const loadIgnoreFile = file => (
fs.readFileSync(file, 'utf8')
.trim()
.split(/\r?\n|\r/)
.filter(x => x.length > 0)
.map(pattern => path.normalize(`${path.dirname(file)}/${pattern}`))
);
const combineIgnores = ignore => next => ({ ...next, ignore: ignore.concat(next.ignore) })
const discoverIgnores = files => {
const ignoreLists = []
const ignoreFiles = {}
for (const file of files) {
const dir = path.dirname(file)
if (!ignoreFiles[dir]) {
const ignoreFile = path.join(dir, '.i18nignore')
ignoreFiles[dir] = true
if (fs.existsSync(ignoreFile)) {
ignoreLists.push(loadIgnoreFile(ignoreFile))
}
}
}
return ignoreLists.flat()
}
const loadConfigFromDirectory = dir => {
const configFile = path.resolve(dir, '.i18nrc')
const ignoreFile = path.resolve(dir, '.i18nignore')
const config = fs.existsSync(configFile) ?
JSON.parse(fs.readFileSync(configFile, 'utf8')) :
{}
;
config.cwd = dir
config.ignore = fs.existsSync(ignoreFile) ? loadIgnoreFile(ignoreFile) : []
return config
}
const scanFilesFromI18nrc = ({ cwd, files = [], ignore = [], include = [] }) => {
const globopts = { cwd, absolute: true }
for (const { pattern, processor } of files) {
// need 2 passes to discover .i18nignore files
const included = glob.sync(pattern, { ignore, ...globopts })
filesByProcessor[processor] = filesByProcessor[processor].concat(
glob.sync(pattern, { ignore: ignore.concat(discoverIgnores(included)), ...globopts })
)
}
for (const dir of include) {
scanFilesFromI18nrc(
combineIgnores(ignore)(
loadConfigFromDirectory(
path.resolve(cwd, dir)
)
)
)
}
}
exports.getFilesForProcessor = name => filesByProcessor[name]
exports.loadConfigFromDirectory = loadConfigFromDirectory
exports.scanFilesFromI18nrc = scanFilesFromI18nrc
exports.reset = () => {
for (const processor of Object.keys(filesByProcessor)) {
filesByProcessor[processor].splice(0)
}
}

View File

@ -1,176 +0,0 @@
/*
* Copyright (C) 2022 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const createScopedTranslateCall = require("./scoped_translate_call")
const Errors = require("./errors");
const { default: I18nJsExtractor } = require("@instructure/i18nliner/dist/lib/extractors/i18n_js_extractor");
const { default: TranslateCall } = require("@instructure/i18nliner/dist/lib/extractors/translate_call");
const ScopedTranslateCall = createScopedTranslateCall(TranslateCall);
const CANVAS_I18N_PACKAGE = '@canvas/i18n'
const CANVAS_I18N_USE_SCOPE_SPECIFIER = 'useScope'
const CANVAS_I18N_RECEIVER = 'I18n'
// This extractor implementation is suitable for ES modules where a module
// imports the "useScope" function from the @canvas/i18n package and assigns the
// output of a call to that function to a receiver named exactly "I18n". Calls
// to the "t" or "translate" methods on that receiver will use the scope
// supplied to that "useScope" call.
//
// import { useScope } from '@canvas/i18n'
//
// const I18n = useScope('foo')
//
// I18n.t('my_key', 'Hello world!')
// // => { "foo": { "my_key": "Hello World" } }
//
// The extractor looks for the I18n receiver defined in the current lexical
// scope of the call to I18n.t():
//
// function a() {
// const I18n = useScope('foo')
// I18n.t('my_key', 'Key in foo') // => foo.my_key
// }
//
// function b() {
// const I18n = useScope('bar')
// I18n.t('my_key', 'Key in bar') // => bar.my_key
// }
//
// Note that the receiver MUST be identified as "I18n". The (base) extractor
// will fail to recognize any translate calls if the output of useScope is
// assigned to a receiver with a different identifier. With that said, the
// identifier for useScope can be renamed at will:
//
// // this is OK:
// import { useScope as useI18nScope } from '@canvas/i18n'
// const I18n = useI18nScope('foo')
//
// // this is NOT ok:
// import { useScope } from '@canvas/i18n'
// const smth = useScope('foo')
//
class ScopedESMExtractor extends I18nJsExtractor {
constructor() {
super(...arguments)
// the identifier for the "useScope" specifier imported from @canvas/i18n,
// which may be renamed
this.useScopeIdentifier = null
// mapping of "I18n" receivers to the (i18n) scopes they were assigned in
// the call to useScope
this.receiverScopeMapping = new WeakMap()
};
enter(path) {
// import { useScope } from '@canvas/i18n'
// ^^^^^^^^
// import { useScope as blah } from '@canvas/i18n'
// ^^^^
if (!this.useScopeIdentifier && path.type === 'ImportDeclaration') {
trackUseScopeIdentifier.call(this, path);
}
// let I18n
// ^^^^
// I18n = useScope('foo')
// ^^^
// (this happens in CoffeeScript when compiled to JS)
else if (this.useScopeIdentifier && path.type === 'AssignmentExpression') {
indexScopeFromAssignment.call(this, path)
}
// const I18n = useScope('foo')
// ^^^^ ^^^
else if (this.useScopeIdentifier && path.type === 'VariableDeclarator') {
indexScopeFromDeclaration.call(this, path)
}
return super.enter(...arguments)
};
buildTranslateCall(line, method, args, path) {
const binding = path.scope.getBinding(CANVAS_I18N_RECEIVER)
const scope = this.receiverScopeMapping.get(binding)
if (scope) {
return new ScopedTranslateCall(line, method, args, scope);
}
else {
throw new Errors.UnscopedTranslateCall(line)
}
};
};
function trackUseScopeIdentifier({ node }) {
if (
node.source &&
node.source.type === 'StringLiteral' &&
node.source.value === CANVAS_I18N_PACKAGE
) {
const specifier = node.specifiers.find(x =>
x.type === 'ImportSpecifier'&&
x.imported &&
x.imported.type === 'Identifier' &&
x.imported.name === CANVAS_I18N_USE_SCOPE_SPECIFIER
)
if (
specifier &&
specifier.type === 'ImportSpecifier' &&
specifier.local &&
specifier.local.type === 'Identifier' &&
specifier.local.name
) {
this.useScopeIdentifier = specifier.local.name
}
}
};
function indexScopeFromAssignment(path) {
return indexScope.call(this, path, path.node.left, path.node.right)
};
function indexScopeFromDeclaration(path) {
return indexScope.call(this, path, path.node.id, path.node.init)
};
// left: Identifier
// right: CallExpression
function indexScope(path, left, right) {
if (
left &&
left.type === 'Identifier' &&
left.name === CANVAS_I18N_RECEIVER &&
right &&
right.type === 'CallExpression' &&
right.callee &&
right.callee.type === 'Identifier' &&
right.callee.name === this.useScopeIdentifier &&
right.arguments &&
right.arguments.length === 1 &&
right.arguments[0].type === 'StringLiteral' &&
right.arguments[0].value
) {
this.receiverScopeMapping.set(
path.scope.getBinding(CANVAS_I18N_RECEIVER),
right.arguments[0].value
)
}
};
module.exports = ScopedESMExtractor;

View File

@ -1,78 +0,0 @@
/*
* Copyright (C) 2014 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var HbsExtractor = require("@instructure/i18nliner-handlebars/dist/lib/extractor").default;
var HbsTranslateCall = require("@instructure/i18nliner-handlebars/dist/lib/t_call").default;
var ScopedHbsTranslateCall = require("./scoped_translate_call")(HbsTranslateCall);
var path = require('path')
var fs = require('fs')
function ScopedHbsExtractor(ast, options) {
// read the scope from the i18nScope property in the accompanying .json file:
this.scope = ScopedHbsExtractor.readI18nScopeFromJSONFile(
// resolve relative to process.cwd() in case it's not absolute
path.resolve(options.path)
)
this.path = options.path // need this for error reporting
HbsExtractor.apply(this, arguments);
};
ScopedHbsExtractor.prototype = Object.create(HbsExtractor.prototype);
ScopedHbsExtractor.prototype.constructor = ScopedHbsExtractor;
ScopedHbsExtractor.prototype.normalizePath = function(path) {
return path;
};
ScopedHbsExtractor.prototype.buildTranslateCall = function(sexpr) {
if (!this.scope) {
const friendlyFile = path.relative(process.cwd(), this.path)
throw new Error(`
canvas_i18nliner: expected i18nScope for Handlebars template to be specified in
the accompanying .json file, but found none:
${friendlyFile}
To fix this, create the following JSON file with the "i18nScope" property set to
the i18n scope to use for the template (e.g. similar to what you'd do in
JavaScript, like \`import I18n from "i18n!foo.bar"\`):
^^^^^^^
// file: ${friendlyFile + '.json'}
{
"i18nScope": "..."
}
`)
}
return new ScopedHbsTranslateCall(sexpr, this.scope);
};
ScopedHbsExtractor.readI18nScopeFromJSONFile = function(filepath) {
const metadataFilepath = `${filepath}.json`
if (fs.existsSync(metadataFilepath)) {
return require(metadataFilepath).i18nScope
}
};
module.exports = ScopedHbsExtractor;

View File

@ -1,54 +0,0 @@
/*
* Copyright (C) 2014 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var I18nlinerHbs = require("@instructure/i18nliner-handlebars").default;
var PreProcessor = require("@instructure/i18nliner-handlebars/dist/lib/pre_processor").default;
var Handlebars = require("handlebars");
var AST = Handlebars.AST;
var StringNode = AST.StringNode;
var HashNode = AST.HashNode;
// slightly more lax interpolation key format for hbs to support any
// existing translations (camel case and dot syntax, e.g. "foo.bar.baz")
PreProcessor.normalizeInterpolationKey = function(key) {
key = key.replace(/[^a-z0-9.]/gi, ' ');
key = key.trim();
key = key.replace(/ +/g, '_');
return key.substring(0, 32);
};
// add explicit scope to all t calls (post block -> inline transformation)
var _processStatement = PreProcessor.processStatement;
PreProcessor.processStatement = function(statement) {
statement = _processStatement.call(this, statement) || statement;
if (statement.type === 'mustache' && statement.id.string === 't')
return this.injectScope(statement);
}
PreProcessor.injectScope = function(node) {
var pairs;
if (!node.hash)
node.hash = node.sexpr.hash = new HashNode([]);
pairs = node.hash.pairs;
// to match our .rb scoping behavior, don't scope inferred keys...
// if inferred, it's always the last option
if (!pairs.length || pairs[pairs.length - 1][0] !== "i18n_inferred_key") {
node.hash.pairs = pairs.concat([["scope", new StringNode(this.scope)]]);
}
return node;
}

View File

@ -1,46 +0,0 @@
/*
* Copyright (C) 2014 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
module.exports = function(TranslateCall) {
var ScopedTranslateCall = function() {
var args = [].slice.call(arguments);
this.scope = args.pop();
TranslateCall.apply(this, arguments);
}
ScopedTranslateCall.prototype = Object.create(TranslateCall.prototype);
ScopedTranslateCall.prototype.constructor = ScopedTranslateCall;
ScopedTranslateCall.prototype.normalizeKey = function(key) {
if (key[0] === '#')
return key.slice(1);
else
return this.scope + "." + key;
};
ScopedTranslateCall.prototype.normalize = function() {
// TODO: make i18nliner-js use the latter, just like i18nliner(.rb) ...
// i18nliner-handlebars can't use the former
if (!this.inferredKey && !this.options.i18n_inferred_key)
this.key = this.normalizeKey(this.key);
TranslateCall.prototype.normalize.call(this);
};
return ScopedTranslateCall;
}

View File

@ -1,83 +0,0 @@
/*
* Copyright (C) 2014 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var TranslationHash = require("@instructure/i18nliner/dist/lib/extractors/translation_hash").default;
function keys(obj) {
var result = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) result.push(key);
}
return result;
}
/* Flatten a deeply nested object, joining intermediate keys with "."
*
* e.g.
*
* flatten({a: 1, b: {c: 2, d: {e: 3}}})
* => {"a": 1", "b.c": 2, "b.d.e": 3}
*/
function flatten(obj, prefix, result) {
result = result || {};
var subPrefix;
var value;
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
var value = obj[key];
fullKey = prefix ? prefix + "." + key : key;
if (value instanceof Object && !(value instanceof Array)) {
flatten(value, fullKey, result);
} else {
result[fullKey] = value;
}
}
}
return result;
}
/* Track a different TranslationHash for each scope.
*
* This is needed for i18n:generate_js, since it builds a separate
* translation bundle for each i18n scope (so the "i18n!scope" magic
* works)
*/
function ScopedTranslationHash() {
this.hashes = {};
this.masterHash = new TranslationHash()
this.translations = this.masterHash.translations;
}
ScopedTranslationHash.prototype.set = function(key, value, meta) {
this.masterHash.set(key, value, meta); // we need this for collision checking
var scope = meta.scope;
var hash = this.hashes[scope] = this.hashes[scope] || new TranslationHash();
hash.set(key, value, meta);
};
ScopedTranslationHash.prototype.keysByScope = function() {
var hash = {};
for (key in this.hashes) {
hash[key] = keys(flatten(this.hashes[key].translations));
}
return hash;
};
module.exports = ScopedTranslationHash;

View File

@ -1,24 +0,0 @@
{
"name": "i18nliner-canvas",
"description": "i18nliner, canvas style",
"private": true,
"main": "./js/main",
"version": "0.0.1",
"dependencies": {
"@instructure/i18nliner-handlebars": "^1.0.3",
"@instructure/i18nliner": "^2.1.0",
"ember-template-compiler": "1.8.0",
"glob": "^7.0.3",
"handlebars": "1.3.0",
"minimist": "^1.1.0",
"mkdirp": "^0.5.1"
},
"devDependencies": {
"coffee-script": "^1.12.4",
"jest": "^26"
},
"peerDependencies": {},
"scripts": {
"test": "jest"
}
}

View File

@ -1,8 +0,0 @@
{
"files": [
{
"pattern": "**/*.hbs",
"processor": "hbs"
}
]
}

View File

@ -1 +0,0 @@
<div>{{t 'foo' 'lulz'}}</div>

View File

@ -1,8 +0,0 @@
{
"files": [
{
"pattern": "**/*.hbs",
"processor": "hbs"
}
]
}

View File

@ -1,7 +0,0 @@
<p>{{#t "#absolute_key"}}Absolute key{{/t}}</p>
<p>{{#t "relative_key"}}Relative key{{/t}}</p>
<p>{{#t}}Inferred key{{/t}}</p>
<p>{{t "#inline_with_absolute_key" "Inline with absolute key"}}</p>
<p>{{t "inline_with_relative_key" "Inline with relative key"}}</p>
<p>{{t "Inline with inferred key"}}</p>

View File

@ -1,3 +0,0 @@
{
"i18nScope": "foo.bar_baz"
}

View File

@ -1 +0,0 @@
<p>{{t "inline_with_relative_key" "Inline with relative key"}}</p>

View File

@ -1,3 +0,0 @@
{
"i18nScope": "foo.bar_fizz_buzz"
}

View File

@ -1,8 +0,0 @@
{
"files": [
{
"pattern": "**/*.{coffee,js,ts,tsx}",
"processor": "js"
}
]
}

View File

@ -1,21 +0,0 @@
#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
import { useScope } from '@canvas/i18n'
I18n = useScope('coffee')
I18n.t("yay coffee")

View File

@ -1,21 +0,0 @@
#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
import { useScope } from '@canvas/i18n'
I18n = useScope('coffee')
I18n.t("yay plzdont")

View File

@ -1,36 +0,0 @@
/*
* Copyright (C) 2022 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useScope } from '@canvas/i18n'
const I18n = useScope('esm')
I18n.t('my_key', 'Hello world')
I18n.t("#absolute_key", "Absolute key");
I18n.t("Inferred key");
I18n.t("nested.relative_key", "Relative key in nested scope");
function a() {
const I18n = useScope('foo')
I18n.t("relative_key", "Relative key");
}
function b() {
const I18n = useScope('bar')
I18n.t("relative_key", "Another relative key");
}

View File

@ -1,24 +0,0 @@
/*
* Copyright (C) 2015 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useScope } from '@canvas/i18n'
let test: string = 'test'
const I18n = useScope('ts')
I18n.t('yay typescript')

View File

@ -1,104 +0,0 @@
/*
* Copyright (C) 2022 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const mkdirp = require("mkdirp");
const { I18nliner } = require("../js/main");
const scanner = require("../js/scanner");
class PanickyCheck extends I18nliner.Commands.Check {
// don't print to TTY
print() {};
// and do throw errors
checkWrapper(file, checker) {
try {
checker(file)
}
catch (e) {
throw new Error(e.message)
}
};
};
var subject = function(dir) {
var command = new PanickyCheck({});
scanner.scanFilesFromI18nrc(scanner.loadConfigFromDirectory(dir))
command.run();
return command.translations.masterHash.translations;
}
describe("I18nliner", function() {
afterEach(function() {
scanner.reset()
})
describe("handlebars", function() {
it("extracts default translations", function() {
expect(subject("spec/fixtures/hbs")).toEqual({
absolute_key: "Absolute key",
inferred_key_c49e3743: "Inferred key",
inline_with_absolute_key: "Inline with absolute key",
inline_with_inferred_key_88e68761: "Inline with inferred key",
foo: {
bar_baz: {
inline_with_relative_key: "Inline with relative key",
relative_key: "Relative key"
},
bar_fizz_buzz: {
inline_with_relative_key: "Inline with relative key"
}
}
});
});
it('throws if no scope was specified', () => {
const command = new PanickyCheck({});
scanner.scanFilesFromI18nrc(
scanner.loadConfigFromDirectory('spec/fixtures/hbs-missing-i18n-scope')
)
expect(() => {
command.checkFiles()
}).toThrowError(/expected i18nScope for Handlebars template to be specified/)
})
});
describe("javascript", function() {
it("extracts default translations", function() {
expect(subject("spec/fixtures/js")).toEqual({
absolute_key: "Absolute key",
inferred_key_c49e3743: "Inferred key",
esm: {
my_key: 'Hello world',
nested: {
relative_key: "Relative key in nested scope"
},
},
foo: {
relative_key: "Relative key"
},
bar: {
relative_key: "Another relative key"
},
yay_coffee_d4d65736: 'yay coffee',
yay_typescript_2a26bb91: 'yay typescript'
});
});
});
});

View File

@ -1,150 +0,0 @@
/*
* Copyright (C) 2022 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const JsProcessor = require('@instructure/i18nliner/dist/lib/processors/js_processor')['default'];
const ScopedESMExtractor = require('../js/scoped_esm_extractor');
const dedent = require('dedent')
describe('ScopedESMExtractor', () => {
it('tracks scope through the call to @canvas/i18n#useScope', () => {
expect(extract(dedent`
import { useScope } from '@canvas/i18n'
const I18n = useScope('foo')
I18n.t('keyed', 'something')
`).translations.translations).toEqual({
foo: {
keyed: 'something'
}
});
})
it('tracks scope through the call to a renamed @canvas/i18n#useScope specifier', () => {
expect(extract(dedent`
import { useScope as useI18nScope } from '@canvas/i18n'
const I18n = useI18nScope('foo')
I18n.t('keyed', 'something')
`).translations.translations).toEqual({
foo: {
keyed: 'something'
}
});
})
it('tracks scope through assignment', () => {
expect(extract(dedent`
import { useScope } from '@canvas/i18n'
let I18n
I18n = useScope('foo')
I18n.t('keyed', 'something')
`).translations.translations).toEqual({
foo: {
keyed: 'something'
}
});
})
it('resolves (i18n) scopes across different lexical scopes', () => {
const { translations: translationHash } = extract(dedent`
import { useScope } from '@canvas/i18n'
function a() {
const I18n = useScope('foo')
I18n.t('key', 'hello')
I18n.t('inferred')
}
function b() {
let I18n
I18n = useScope('bar')
I18n.t('key', 'world')
I18n.t('inferred')
}
`)
expect(translationHash.translations).toEqual({
foo: { key: 'hello' },
bar: { key: 'world' },
inferred_7cf5962e: 'inferred'
})
})
it('extracts translations', () => {
expect(extract(dedent`
import { useScope } from '@canvas/i18n'
const I18n = useScope('foo')
I18n.t('#absolute', 'Unscoped')
I18n.t('inferred')
I18n.t('keyed', 'Keyed')
I18n.t('nested.keyed', 'Nested')
`).translations.translations).toEqual({
absolute: 'Unscoped',
foo: {
keyed: 'Keyed',
nested: {
keyed: 'Nested'
}
},
inferred_7cf5962e: 'inferred'
});
})
it('throws if no scope was defined by the time t() was called', () => {
expect(() => {
extract(dedent`
import I18n from '@canvas/i18n'
I18n.t('hello')
`)
}).toThrow(/unscoped translate call/);
})
it('throws if no scope was defined using the "useScope" specifier', () => {
expect(() => {
extract(dedent`
import { useScope } from '@canvas/i18n'
const I18n = somethingElse('foo')
I18n.t('hello')
`)
}).toThrow(/unscoped translate call/);
})
it('throws if no scope was defined using the "useScope" interface from @canvas/i18n', () => {
expect(() => {
extract(dedent`
function useScope() {}
const I18n = useScope('foo')
I18n.t('hello')
`)
}).toThrow(/unscoped translate call/);
})
});
function extract(source) {
const extractor = new ScopedESMExtractor({
ast: JsProcessor.prototype.parse(source)
})
extractor.run()
return extractor;
}

View File

@ -1,32 +0,0 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const HbsProcessor = require('@instructure/i18nliner-handlebars/dist/lib/hbs_processor')['default'];
const ScopedHbsExtractor = require('../js/scoped_hbs_extractor');
const Handlebars = require('handlebars')
const path = require('path')
describe('ScopedHbsExtractor.readI18nScopeFromJSONFile', () => {
it('reads the i18nScope from the accompanying .json file', () => {
expect(
ScopedHbsExtractor.readI18nScopeFromJSONFile(
path.resolve(__dirname, 'fixtures/hbs/app/views/jst/foo/_barBaz.hbs')
)
).toEqual('foo.bar_baz')
})
});

View File

@ -1,5 +0,0 @@
#!/bin/bash
set -e
yarn install || yarn install --network-concurrency 1
yarn test

View File

@ -25,13 +25,19 @@ namespace :i18n do
# @instructure/i18nliner on the frontend.
source_translations_file = Rails.root.join("config/locales/generated/en.yml").to_s
js_i18nliner_path = Rails.root.join("gems/canvas_i18nliner/bin/i18nliner").to_s
js_i18nliner_path = Rails.root.join("node_modules/@instructure/i18nliner-canvas/bin/i18nliner").to_s
# Translations extracted from the frontend source code.
#
# This file has a hierarchical structure, unlike the "index" one. It looks
# similar to what the Ruby I18nliner exports.
js_translations_file = Rails.root.join("config/locales/generated/en.json").to_s
js_translations_file = Rails.root.join("config/locales/generated/en-js.json").to_s
# Input to the routine that generates JS modules for every locale, that are
# then loaded at runtime by the frontend.
#
# See @instructure/i18nliner-canvas for the structure of this file.
js_index_file = Rails.root.join("config/locales/generated/en-js-index.json").to_s
# Directory to contain the auto-generated translation files for the frontend.
js_translation_modules_dir = Rails.root.join("public/javascripts/translations").to_s
@ -101,7 +107,8 @@ namespace :i18n do
task extract_js: [] do
exit 1 unless system(
js_i18nliner_path, "export",
"--translationsFile", js_translations_file
"--translationsFile", js_translations_file,
"--indexFile", js_index_file
)
end
@ -111,7 +118,7 @@ namespace :i18n do
task generate: [:extract]
desc "Generates JS bundle i18n files (non-en) and adds them to assets.yml"
task generate_js: :i18n_environment do
task generate_js: [:i18n_environment, :extract_js] do
locales = I18n.available_locales
if locales.empty?
@ -119,17 +126,25 @@ namespace :i18n do
exit 0
end
system "./gems/canvas_i18nliner/bin/i18nliner generate_js"
FileUtils.mkdir_p(js_translation_modules_dir)
if $?.exitstatus > 0
warn "Error extracting JS translations; confirm that `./gems/canvas_i18nliner/bin/i18nliner generate_js` works"
exit $?.exitstatus
# emulate the old "js_bundles.json" file, which was a mapping of each scope
# to the phrases it uses + defines
scope_keys = JSON.parse(File.read(js_index_file)).each_with_object({}) do |phrase, acc|
acc[phrase["scope"]] ||= []
acc[phrase["scope"]].push(phrase["key"]) unless acc[phrase["scope"]].include?(phrase["key"])
if phrase.key?("used_in")
acc[phrase["used_in"]] ||= []
acc[phrase["used_in"]].push(phrase["key"])
acc[phrase["used_in"]].push(phrase["key"]) unless acc[phrase["used_in"]].include?(phrase["key"])
end
end
I18nTasks::GenerateJs.new.apply(
locales: locales,
translations: I18n.backend.send(:translations),
scope_keys: JSON.parse(File.read("config/locales/generated/js_bundles.json"))
scope_keys: scope_keys
).each do |(filename, content)|
file = "#{js_translation_modules_dir}/#{filename}.js"
if !File.exist?(file) || File.read(file) != content

View File

@ -9,7 +9,6 @@
"private": true,
"workspaces": {
"packages": [
"gems/canvas_i18nliner",
"gems/plugins/*",
"packages/*",
"ui/shared/*"
@ -152,6 +151,7 @@
"parse-link-header": "^1",
"prop-types": "^15",
"qs": "^6.6.0",
"querystring": "0.2.1",
"react": "^16.13.1",
"react-apollo": "~3.0.1",
"react-dnd": "^2.5.2",
@ -205,6 +205,10 @@
"@babel/preset-react": "7.14.5",
"@babel/preset-typescript": "^7.14.5",
"@instructure/browserslist-config-canvas-lms": ">=2",
"@instructure/i18nliner": "^3",
"@instructure/i18nliner-canvas": "^1.2",
"@instructure/i18nliner-handlebars": "^2",
"@instructure/i18nliner-runtime": "^1",
"@prettier/plugin-ruby": "^1.5.2",
"@sentry/webpack-plugin": "^1.5.2",
"@sheerun/mutationobserver-shim": "0.3.2",

View File

@ -1,7 +0,0 @@
bower/**/*
compiled/**/*
i18nObj.js
plugins/**/*
symlink_to_node_modules/**/*
translations/**/*
vendor/**/*

View File

@ -1,8 +0,0 @@
{
"files": [
{
"pattern": "**/*.js",
"processor": "js"
}
]
}

View File

@ -41,7 +41,6 @@ module.exports = {
mode: 'development',
module: {
noParse: [
require.resolve('@instructure/i18nliner/dist/lib/i18nliner.js'),
require.resolve('jquery'),
require.resolve('tinymce'),
],

View File

@ -25,8 +25,9 @@
const path = require('path')
const Handlebars = require('handlebars')
const EmberHandlebars = require('ember-template-compiler').EmberHandlebars
const ScopedHbsExtractor = require('i18nliner-canvas/js/scoped_hbs_extractor')
const PreProcessor = require('@instructure/i18nliner-handlebars/dist/lib/pre_processor').default
const {readI18nScopeFromJSONFile} = require('@instructure/i18nliner-canvas/scoped_hbs_resolver')
const ScopedHbsExtractor = require('@instructure/i18nliner-canvas/scoped_hbs_extractor')
const ScopedHbsPreProcessor = require('@instructure/i18nliner-canvas/scoped_hbs_pre_processor')
const { canvasDir } = require('#params')
function compileHandlebars(data) {
@ -34,10 +35,9 @@ function compileHandlebars(data) {
try {
let translationCount = 0
const ast = Handlebars.parse(source)
const extractor = new ScopedHbsExtractor(ast, {path})
const scope = extractor.scope
PreProcessor.scope = scope
PreProcessor.process(ast)
const scope = readI18nScopeFromJSONFile(path)
const extractor = new ScopedHbsExtractor(ast, {path, scope})
ScopedHbsPreProcessor.processWithScope(scope, ast)
extractor.forEach(() => translationCount++)
const precompiler = data.ember ? EmberHandlebars : Handlebars
@ -55,11 +55,12 @@ function resourceName(path) {
return path.replace(/^.+?\/templates\//, '').replace(/\.hbs$/, '')
}
function emitTemplate(path, name, result, dependencies) {
function emitTemplate({ name, template, dependencies }) {
return `
import Ember from 'ember';
${dependencies.map(d => `import ${JSON.stringify(d)};`).join('\n')}
const template = Ember.Handlebars.template(${result.template});
const template = Ember.Handlebars.template(${template});
Ember.TEMPLATES['${name}'] = template;
export default template;
`
@ -91,6 +92,6 @@ module.exports = function(source) {
if (result.translationCount > 0) {
dependencies.push('@canvas/i18n')
}
const compiledTemplate = emitTemplate(this.resourcePath, name, result, dependencies)
return compiledTemplate
return emitTemplate({ name, template: result.template, dependencies })
}

View File

@ -24,20 +24,13 @@
const Handlebars = require('handlebars')
const {pick} = require('lodash')
const {EmberHandlebars} = require('ember-template-compiler')
const ScopedHbsExtractor = require('i18nliner-canvas/js/scoped_hbs_extractor')
const PreProcessor = require('@instructure/i18nliner-handlebars/dist/lib/pre_processor').default
const ScopedHbsExtractor = require('@instructure/i18nliner-canvas/scoped_hbs_extractor')
const ScopedHbsPreProcessor = require('@instructure/i18nliner-canvas/scoped_hbs_pre_processor')
const {readI18nScopeFromJSONFile} = require('@instructure/i18nliner-canvas/scoped_hbs_resolver')
const nodePath = require('path')
const loaderUtils = require('loader-utils')
const { canvasDir } = require('#params')
const { contriveId, config: brandableCSSConfig } = requireBrandableCSS()
require('i18nliner-canvas/js/scoped_hbs_pre_processor')
// In this main file, we do a bunch of stuff to monkey-patch the default behavior of
// i18nliner's HbsProcessor (specifically, we set the the `directories` and define a
// `normalizePath` function so that translation keys stay relative to canvas root dir).
// By requiring it here the code here will use that monkeypatched behavior.
require('i18nliner-canvas/js/main')
const compileHandlebars = data => {
const path = data.path
@ -45,10 +38,9 @@ const compileHandlebars = data => {
try {
let translationCount = 0
const ast = Handlebars.parse(source)
const extractor = new ScopedHbsExtractor(ast, {path})
const scope = extractor.scope
PreProcessor.scope = scope
PreProcessor.process(ast)
const scope = readI18nScopeFromJSONFile(path)
const extractor = new ScopedHbsExtractor(ast, {path, scope})
ScopedHbsPreProcessor.processWithScope(scope, ast)
extractor.forEach(() => translationCount++)
const precompiler = data.ember ? EmberHandlebars : Handlebars
@ -60,15 +52,22 @@ const compileHandlebars = data => {
}
}
const emitTemplate = (path, name, result, dependencies, cssRegistration, partialRegistration) => {
const emitTemplate = ({
name,
template,
dependencies,
cssRegistration,
partialRegistration,
}) => {
return `
import _Handlebars from 'handlebars/runtime';
var Handlebars = _Handlebars.default;
${dependencies.map(d => `import ${JSON.stringify(d)};`).join('\n')}
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
var name = '${name}';
templates[name] = template(${result.template});
templates[name] = template(${template});
${partialRegistration};
${cssRegistration};
export default templates[name];
@ -169,15 +168,13 @@ function i18nLinerHandlebarsLoader(source) {
// make sure the template has access to all our handlebars helpers
dependencies.push('@canvas/handlebars-helpers/index.coffee')
const compiledTemplate = emitTemplate(
this.resourcePath,
return emitTemplate({
name,
result,
template: result.template,
dependencies,
cssRegistration,
partialRegistration
)
return compiledTemplate
partialRegistration,
})
}
function requireBrandableCSS() {

View File

@ -167,7 +167,10 @@ module.exports = {
fallback: {
// for minimatch module; it can work without path so let webpack know
// instead of trying to resolve node's "path"
path: false
path: false,
// for parse-link-header, which requires "querystring" which is a node
// module. btw we have at least 3 implementations of "parse-link-header"!
querystring: require.resolve('querystring-es3')
},
modules: [
@ -191,8 +194,6 @@ module.exports = {
// The files are expected to have no call to require, define or similar.
// They are allowed to use exports and module.exports.
noParse: [
// i18nLiner has a `require('fs')` that it doesn't actually need, ignore it.
require.resolve('@instructure/i18nliner/dist/lib/i18nliner.js'),
require.resolve('jquery'),
require.resolve('tinymce'),
],

View File

@ -47,6 +47,8 @@ Handlebars.registerHelper name, fn for name, fn of {
options.wrapper = wrappers if wrappers['*']
unless (typeof this == 'undefined') || (this instanceof Window)
options[key] = this[key] for key in this
if options.i18n_scope
useI18nScope(options.i18n_scope)
new Handlebars.SafeString htmlEscape(I18nObj.t(args..., options))
__i18nliner_escape: (val) ->

View File

@ -16,16 +16,50 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'date-js'
import $ from 'jquery'
import i18nLolcalize from './i18nLolcalize'
import I18n from 'i18n-js'
import extend from '@instructure/i18nliner/dist/lib/extensions/i18n_js'
import {
extend as activateI18nliner,
inferKey,
normalizeDefault
} from '@instructure/i18nliner-runtime'
import logEagerLookupViolations from './logEagerLookupViolations'
import htmlEscape from 'html-escape'
import 'date-js'
// add i18nliner's runtime extensions to the global I18n object
extend(I18n)
activateI18nliner(I18n, {
// this is what we use elsewhere in canvas, so make i18nliner use it too
HtmlSafeString: htmlEscape.SafeString,
// handle our absolute keys
keyPattern: /^\#?\w+(\.\w+)+$/,
inferKey: (defaultValue, translateOptions) => (
`#${inferKey(defaultValue, translateOptions)}`
),
// when inferring the key at runtime (i.e. js/coffee or inline hbs `t`
// call), signal to normalizeKey that it shouldn't be scoped.
normalizeKey: (key, options) => {
if (key[0] === '#') {
delete options.scope
return key.slice(1)
}
else if (options.scope) {
const { scope } = options
delete options.scope
return `${scope}.${key}`
}
else {
return key
}
},
normalizeDefault: (window.ENV && window.ENV.lolcalize) ?
i18nLolcalize :
normalizeDefault
})
/*
* Overridden interpolator that localizes any interpolated numbers.
@ -37,10 +71,10 @@ const interpolate = I18n.interpolate.bind(I18n)
I18n.interpolate = function(message, origOptions) {
const options = {...origOptions}
const matches = message.match(this.PLACEHOLDER) || []
const matches = message.match(I18n.placeholder) || []
matches.forEach(placeholder => {
const name = placeholder.replace(this.PLACEHOLDER, '$1')
const name = placeholder.replace(I18n.placeholder, '$1')
if (typeof options[name] === 'number') {
options[name] = this.localizeNumber(options[name])
}
@ -334,29 +368,6 @@ I18n.pluralize = function(count, scope, options) {
return this.interpolate(message, options)
}
I18n.Utils.HtmlSafeString = htmlEscape.SafeString // this is what we use elsewhere in canvas, so make i18nliner use it too
I18n.CallHelpers.keyPattern = /^\#?\w+(\.\w+)+$/ // handle our absolute keys
// when inferring the key at runtime (i.e. js/coffee or inline hbs `t`
// call), signal to normalizeKey that it shouldn't be scoped.
// TODO: make i18nliner-js set i18n_inferred_key, which will DRY things up
// slightly
const inferKey = I18n.CallHelpers.inferKey.bind(I18n.CallHelpers)
I18n.CallHelpers.inferKey = (defaultValue, translateOptions) =>
`#${inferKey(defaultValue, translateOptions)}`
I18n.CallHelpers.normalizeKey = (key, options) => {
if (key[0] === '#') {
key = key.slice(1)
delete options.scope
}
return key
}
if (window.ENV && window.ENV.lolcalize) {
I18n.CallHelpers.normalizeDefault = i18nLolcalize
}
I18n.scoped = I18n.useScope = (scope, callback) => {
const preloadLocale = window.ENV && window.ENV.LOCALE ? window.ENV.LOCALE : 'en'
const i18n_scope = new I18n.scope(scope)

View File

@ -1555,24 +1555,45 @@
dependencies:
"@babel/runtime" "^7.9.2"
"@instructure/i18nliner-handlebars@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@instructure/i18nliner-handlebars/-/i18nliner-handlebars-1.0.3.tgz#0b9f231386bbe013b855a959b35a4e2fe1542095"
integrity sha512-4JiqZ+kQUOG6D3Df4/UQOtpG/ZDMYW4vmtkaX4Jlkef/1suvPV3JBEyL2Y9rqY/Xvx6K5wp4i6k3FPs7HeCUUw==
"@instructure/i18nliner-canvas@^1.2":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@instructure/i18nliner-canvas/-/i18nliner-canvas-1.2.0.tgz#630165d51515ac014c71e9a2b9301ba6c53cacc5"
integrity sha512-oIi7AY0H0g7IRMb5XEEkR1/YuKDhJ53RR4SiI7WTEz10UstLj1xD0mnXEsfdDv67uiOfDXpXWzQ0QBHFk24l2w==
dependencies:
"@instructure/i18nliner" "^3.0.1"
"@instructure/i18nliner-handlebars" "^2.0.0"
"@instructure/i18nliner-runtime" "^1"
ember-template-compiler "1.8.0"
glob "^7.0.3"
handlebars "1.3.0"
minimist "^1.1.0"
mkdirp "^0.5.1"
"@instructure/i18nliner@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@instructure/i18nliner/-/i18nliner-2.1.0.tgz#c225797d49882b4a9b10bffc795a664d284b9e3a"
integrity sha512-itJCIILSn68y0SjuYNnMfQvG4SvCMVxs6B25ag78lWUV3jaoWMPJ+Lc6ehjDYxI4Uyxac6IYLuyicGRxSZpJKg==
"@instructure/i18nliner-handlebars@^2", "@instructure/i18nliner-handlebars@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@instructure/i18nliner-handlebars/-/i18nliner-handlebars-2.0.0.tgz#76d01fe4609d3247d4829b58f2439954f673399b"
integrity sha512-y2QA1mWgYF4tg/EDmUFS74nfFo/TT6THzKtR82UFGOVxmuKz6ULCk3gH8P/o3bTgebyOdh+S9yKyzLSrZ18MbA==
"@instructure/i18nliner-runtime@^1":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@instructure/i18nliner-runtime/-/i18nliner-runtime-1.0.0.tgz#22f3da94fcb9369deed21bf03a04b35e9469d452"
integrity sha512-BH5Ze6zkZG5mgKB8K4nlSla7Jfdxo5qRzgCIBRBQbnLHPayMpjRK7zAFDi6twxlOBF3Scn6JyeBT0XlMb930GQ==
dependencies:
crc32 "^0.2.2"
speakingurl "^13.0.0"
"@instructure/i18nliner@^3", "@instructure/i18nliner@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@instructure/i18nliner/-/i18nliner-3.0.1.tgz#47d0ed1b7bd6336aa554de3b6ea981ce24f5c534"
integrity sha512-NnryTuMZ0EPyYsIOZ8eC1S4RXqv1kQmMpHjRY/mKhSdRdV6Vpj6pdEsvLTYcWWVpVfQUVz5HQItmQIv93Oq2gg==
dependencies:
"@babel/parser" "^7"
"@babel/traverse" "^7"
"@instructure/i18nliner-runtime" "^1"
cli-color "^1"
crc32 "~0.2.2"
gglobby "0.0.3"
minimist "~1.2.0"
mkdirp "~0.5.1"
speakingurl "13.0.0"
"@instructure/instructure-theme@^7.14.0":
version "7.14.0"
@ -8920,7 +8941,7 @@ coffee-loader@~0.7.2:
dependencies:
loader-utils "^1.0.2"
coffee-script@^1, coffee-script@^1.12.4:
coffee-script@^1:
version "1.12.7"
resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.7.tgz#c05dae0cb79591d05b3070a8433a98c9a89ccc53"
integrity sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==
@ -9424,7 +9445,7 @@ cpy@^8.1.1:
p-filter "^2.1.0"
p-map "^3.0.0"
crc32@^0.2.2, crc32@~0.2.2:
crc32@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/crc32/-/crc32-0.2.2.tgz#7ad220d6ffdcd119f9fc127a7772cacea390a4ba"
integrity sha1-etIg1v/c0Rn5/BJ6d3LKzqOQpLo=
@ -20811,7 +20832,7 @@ querystring@0.2.0:
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
querystring@^0.2.0:
querystring@0.2.1, querystring@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd"
integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==
@ -23176,10 +23197,10 @@ spdx-license-ids@^3.0.0:
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz#8a595135def9592bda69709474f1cbeea7c2467f"
integrity sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==
speakingurl@13.0.0:
speakingurl@^13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-13.0.0.tgz#266c52d2b47585375af058e4783c8d1097a2d3db"
integrity sha1-JmxS0rR1hTda8FjkeDyNEJei09s=
integrity sha512-bjwu8erR5rsfHW9ZKHReHJt9CEPrJCZopJ3rMVcCh8buJom+BI/7bHWYiHSZd+yS9rx4DGN+r5oKoVqDkiqfWw==
specificity@^0.4.1:
version "0.4.1"