Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ivy): i18n - inline current locale at compile-time (#33314)
During compile-time translation inlining, the `$localize.locale` expression will now be replaced with a string literal containing the current locale of the translations. PR Close #33314
- Loading branch information
1 parent
f17072c
commit fb84ea7
Showing
5 changed files
with
220 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
packages/localize/src/tools/src/translate/source_files/locale_plugin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
import {NodePath, PluginObj} from '@babel/core'; | ||
import {MemberExpression, stringLiteral} from '@babel/types'; | ||
|
||
import {TranslatePluginOptions, isLocalize} from './source_file_utils'; | ||
|
||
/** | ||
* This Babel plugin will replace the following code forms with a string literal containing the | ||
* given `locale`. | ||
* | ||
* * `$localize.locale` -> `"locale"` | ||
* * `typeof $localize !== "undefined" && $localize.locale` -> `"locale"` | ||
* * `xxx && typeof $localize !== "undefined" && $localize.locale` -> `"xxx && locale"` | ||
* * `$localize.locale || default` -> `"locale" || default` | ||
* | ||
* @param locale The name of the locale to inline into the code. | ||
* @param options Additional options including the name of the `$localize` function. | ||
*/ | ||
export function makeLocalePlugin( | ||
locale: string, {localizeName = '$localize'}: TranslatePluginOptions = {}): PluginObj { | ||
return { | ||
visitor: { | ||
MemberExpression(expression: NodePath<MemberExpression>) { | ||
const obj = expression.get('object'); | ||
if (!isLocalize(obj, localizeName)) { | ||
return; | ||
} | ||
const property = expression.get('property') as NodePath; | ||
if (!property.isIdentifier({name: 'locale'})) { | ||
return; | ||
} | ||
if (expression.parentPath.isAssignmentExpression() && | ||
expression.parentPath.get('left') === expression) { | ||
return; | ||
} | ||
// Check for the `$localize.locale` being guarded by a check on the existence of | ||
// `$localize`. | ||
const parent = expression.parentPath; | ||
if (parent.isLogicalExpression({operator: '&&'}) && parent.get('right') === expression) { | ||
const left = parent.get('left'); | ||
if (isLocalizeGuard(left, localizeName)) { | ||
// Replace `typeof $localize !== "undefined" && $localize.locale` with | ||
// `$localize.locale` | ||
parent.replaceWith(expression); | ||
} else if ( | ||
left.isLogicalExpression({operator: '&&'}) && | ||
isLocalizeGuard(left.get('right'), localizeName)) { | ||
// The `$localize` is part of a preceding logical AND. | ||
// Replace XXX && typeof $localize !== "undefined" && $localize.locale` with `XXX && | ||
// $localize.locale` | ||
left.replaceWith(left.get('left')); | ||
} | ||
} | ||
// Replace the `$localize.locale` with the string literal | ||
expression.replaceWith(stringLiteral(locale)); | ||
} | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
* Returns true if the expression one of: | ||
* * `typeof $localize !== "undefined"` | ||
* * `"undefined" !== typeof $localize` | ||
* * `typeof $localize != "undefined"` | ||
* * `"undefined" != typeof $localize` | ||
* | ||
* @param expression the expression to check | ||
* @param localizeName the name of the `$localize` symbol | ||
*/ | ||
function isLocalizeGuard(expression: NodePath, localizeName: string): boolean { | ||
if (!expression.isBinaryExpression() || | ||
!(expression.node.operator === '!==' || expression.node.operator === '!=')) { | ||
return false; | ||
} | ||
const left = expression.get('left'); | ||
const right = expression.get('right'); | ||
|
||
return (left.isUnaryExpression({operator: 'typeof'}) && | ||
isLocalize(left.get('argument'), localizeName) && | ||
right.isStringLiteral({value: 'undefined'})) || | ||
(right.isUnaryExpression({operator: 'typeof'}) && | ||
isLocalize(right.get('argument'), localizeName) && | ||
left.isStringLiteral({value: 'undefined'})); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
packages/localize/src/tools/test/translate/source_files/locale_plugin_spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
import {transformSync} from '@babel/core'; | ||
import {makeLocalePlugin} from '../../../src/translate/source_files/locale_plugin'; | ||
|
||
describe('makeLocalePlugin', () => { | ||
it('should replace $localize.locale with the locale string', () => { | ||
const input = '$localize.locale;'; | ||
const output = transformSync(input, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output.code).toEqual('"fr";'); | ||
}); | ||
|
||
it('should replace $localize.locale with the locale string in the context of a variable assignment', | ||
() => { | ||
const input = 'const a = $localize.locale;'; | ||
const output = transformSync(input, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output.code).toEqual('const a = "fr";'); | ||
}); | ||
|
||
it('should replace $localize.locale with the locale string in the context of a binary expression', | ||
() => { | ||
const input = '$localize.locale || "default";'; | ||
const output = transformSync(input, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output.code).toEqual('"fr" || "default";'); | ||
}); | ||
|
||
it('should remove reference to `$localize` if used to guard the locale', () => { | ||
const input = 'typeof $localize !== "undefined" && $localize.locale;'; | ||
const output = transformSync(input, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output.code).toEqual('"fr";'); | ||
}); | ||
|
||
it('should remove reference to `$localize` if used in a longer logical expression to guard the locale', | ||
() => { | ||
const input1 = 'x || y && typeof $localize !== "undefined" && $localize.locale;'; | ||
const output1 = transformSync(input1, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output1.code).toEqual('x || y && "fr";'); | ||
|
||
const input2 = 'x || y && "undefined" !== typeof $localize && $localize.locale;'; | ||
const output2 = transformSync(input2, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output2.code).toEqual('x || y && "fr";'); | ||
|
||
const input3 = 'x || y && typeof $localize != "undefined" && $localize.locale;'; | ||
const output3 = transformSync(input3, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output3.code).toEqual('x || y && "fr";'); | ||
|
||
const input4 = 'x || y && "undefined" != typeof $localize && $localize.locale;'; | ||
const output4 = transformSync(input4, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output4.code).toEqual('x || y && "fr";'); | ||
}); | ||
|
||
it('should ignore properties on $localize other than `locale`', () => { | ||
const input = '$localize.notLocale;'; | ||
const output = transformSync(input, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output.code).toEqual('$localize.notLocale;'); | ||
}); | ||
|
||
it('should ignore indexed property on $localize', () => { | ||
const input = '$localize["locale"];'; | ||
const output = transformSync(input, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output.code).toEqual('$localize["locale"];'); | ||
}); | ||
|
||
it('should ignore `locale` on objects other than $localize', () => { | ||
const input = '$notLocalize.locale;'; | ||
const output = transformSync(input, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output.code).toEqual('$notLocalize.locale;'); | ||
}); | ||
|
||
it('should ignore `$localize.locale` if `$localize` is not global', () => { | ||
const input = 'const $localize = {};\n$localize.locale;'; | ||
const output = transformSync(input, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output.code).toEqual('const $localize = {};\n$localize.locale;'); | ||
}); | ||
|
||
it('should ignore `locale` if it is not directly accessed from `$localize`', () => { | ||
const input = 'const {locale} = $localize;\nconst a = locale;'; | ||
const output = transformSync(input, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output.code).toEqual('const {\n locale\n} = $localize;\nconst a = locale;'); | ||
}); | ||
|
||
it('should ignore `$localize.locale` on LHS of an assignment', () => { | ||
const input = 'let a;\na = $localize.locale = "de";'; | ||
const output = transformSync(input, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output.code).toEqual('let a;\na = $localize.locale = "de";'); | ||
}); | ||
|
||
it('should handle `$localize.locale on RHS of an assignment', () => { | ||
const input = 'let a;\na = $localize.locale;'; | ||
const output = transformSync(input, {plugins: [makeLocalePlugin('fr')]}) !; | ||
expect(output.code).toEqual('let a;\na = "fr";'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters