Skip to content

Commit

Permalink
Merge pull request #25650 from mshima/translation
Browse files Browse the repository at this point in the history
add simplified and improved translation process.
  • Loading branch information
DanielFran authored Mar 28, 2024
2 parents 404daa7 + 8445eb7 commit 9147e93
Show file tree
Hide file tree
Showing 14 changed files with 343 additions and 57 deletions.
3 changes: 2 additions & 1 deletion generators/angular/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,14 @@ export default class AngularGenerator extends BaseApplicationGenerator {
this.localEntities = entities.filter(entity => !entity.builtIn && !entity.skipClient);
},
queueTranslateTransform({ control, application }) {
const { enableTranslation, jhiPrefix } = application;
this.queueTransformStream(
{
name: 'translating angular application',
filter: file => isFileStateModified(file) && file.path.startsWith(this.destinationPath()) && isTranslatedAngularFile(file),
refresh: false,
},
translateAngularFilesTransform(control.getWebappTranslation, application.enableTranslation),
translateAngularFilesTransform(control.getWebappTranslation, { enableTranslation, jhiPrefix }),
);
},
});
Expand Down
1 change: 0 additions & 1 deletion generators/angular/support/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,4 @@ export * from './needles.js';
export * from './path-utils.js';
export * from './reserved-keywords.js';
export * from './translate-angular.js';
export { default as translateAngularFilesTransform } from './translate-angular.js';
export { default as updateLanguagesTask } from './update-languages.js';
102 changes: 97 additions & 5 deletions generators/angular/support/translate-angular.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { inspect } from 'node:util';
import { it, describe, expect, esmocha, beforeEach } from 'esmocha';
import { createTranslationReplacer } from './translate-angular.js';

describe('generator - angular - transform', () => {
describe('replaceAngularTranslations', () => {
let replaceAngularTranslations;
let enabledAngularTranslations;

beforeEach(() => {
let value = 0;
replaceAngularTranslations = createTranslationReplacer(esmocha.fn().mockImplementation(key => `translated-value-${key}-${value++}`));
const testImpl = (key, data) => `translated-value-${key}-${data ? `${inspect(data)}-` : ''}${value++}`;
replaceAngularTranslations = createTranslationReplacer(esmocha.fn().mockImplementation(testImpl), {
jhiPrefix: 'jhi',
enableTranslation: false,
});
enabledAngularTranslations = createTranslationReplacer(esmocha.fn().mockImplementation(testImpl), {
jhiPrefix: 'jhi',
enableTranslation: true,
});
});

describe('with translation disabled', () => {
Expand All @@ -39,8 +49,8 @@ describe('generator - angular - transform', () => {
`;
expect(replaceAngularTranslations(body, extension)).toMatchInlineSnapshot(`
"
<h1>translated-value-activate.title1-0</h1>
<h1>translated-value-activate.title2-1</h1>
<h1>translated-value-activate.title1-1</h1>
<h1>translated-value-activate.title2-0</h1>
"
`);
});
Expand Down Expand Up @@ -110,7 +120,7 @@ describe('generator - angular - transform', () => {
`);
});

it('should remove placeholder attribute value with translated value', () => {
it('should replace placeholder attribute value with translated value', () => {
const body = `
<input placeholder="{{ 'global.form.currentpassword.placeholder1' | translate }}"/>
<input placeholder="{{ 'global.form.currentpassword.placeholder2' | translate }}"/>
Expand All @@ -123,7 +133,7 @@ describe('generator - angular - transform', () => {
`);
});

it('should remove title attribute value with translated value', () => {
it('should replace title attribute value with translated value', () => {
const body = `
<input title="{{ 'global.form.currentpassword.title1' | translate }}"/>
<input title="{{ 'global.form.currentpassword.title2' | translate }}"/>
Expand All @@ -133,6 +143,88 @@ describe('generator - angular - transform', () => {
<input title="translated-value-global.form.currentpassword.title1-0"/>
<input title="translated-value-global.form.currentpassword.title2-1"/>
"
`);
});

it('should replace __jhiTranslatePipe__ with translated value', () => {
const body = `
<input title="__jhiTranslatePipe__('global.form.currentpassword.title1')"/>
<input title="__jhiTranslatePipe__('global.form.currentpassword.title2')"/>
`;
expect(replaceAngularTranslations(body, extension)).toMatchInlineSnapshot(`
"
<input title="translated-value-global.form.currentpassword.title1-1"/>
<input title="translated-value-global.form.currentpassword.title2-0"/>
"
`);
});

it('should replace __jhiTranslatePipe__ with translation pipe', () => {
const body = `
<input title="__jhiTranslatePipe__('global.form.currentpassword.title1')"/>
<input title="__jhiTranslatePipe__('global.form.currentpassword.title2')"/>
`;
expect(enabledAngularTranslations(body, extension)).toMatchInlineSnapshot(`
"
<input title="{{ 'global.form.currentpassword.title1' | translate }}"/>
<input title="{{ 'global.form.currentpassword.title2' | translate }}"/>
"
`);
});

it('should replace __jhiTranslateTag__ with translated value', () => {
const body = `
<tag>__jhiTranslateTag__('global.form.currentpassword.title1', { "username": "account()!.login" })</tag>
<tag>
__jhiTranslateTag__('global.form.currentpassword.title2')
</tag>
`;
expect(replaceAngularTranslations(body, extension)).toMatchInlineSnapshot(`
"
<tag>translated-value-global.form.currentpassword.title1-{ username: &apos;{{ account()!.login }}&apos; }-1</tag>
<tag>
translated-value-global.form.currentpassword.title2-0
</tag>
"
`);
});

it('should replace __jhiTranslateTag__ with translation attribute and value', () => {
const body = `
<tag>__jhiTranslateTag__('global.form.currentpassword.title1', { "username": "account()!.login" })</tag>
<tag>
__jhiTranslateTag__('global.form.currentpassword.title2')
</tag>
`;
expect(enabledAngularTranslations(body, extension)).toMatchInlineSnapshot(`
"
<tag jhiTranslate="global.form.currentpassword.title1" [translateValues]="{ username: account()!.login }">translated-value-global.form.currentpassword.title1-{ username: &apos;{{ account()!.login }}&apos; }-1</tag>
<tag jhiTranslate="global.form.currentpassword.title2">
translated-value-global.form.currentpassword.title2-0
</tag>
"
`);
});

it('should replace __jhiTranslateTagPipe__ with translated value', () => {
const body = `
<tag>__jhiTranslateTagPipe__('global.form.currentpassword.title1', { "username": "translation.key" })</tag>
`;
expect(replaceAngularTranslations(body, extension)).toMatchInlineSnapshot(`
"
<tag>translated-value-global.form.currentpassword.title1-{ username: &apos;translated-value-translation.key-0&apos; }-1</tag>
"
`);
});

it('should replace __jhiTranslateTagPipe__ with translation attribute and value', () => {
const body = `
<tag>__jhiTranslateTagPipe__('global.form.currentpassword.title1', { "username": "translation.key" })</tag>
`;
expect(enabledAngularTranslations(body, extension)).toMatchInlineSnapshot(`
"
<tag jhiTranslate="global.form.currentpassword.title1" [translateValues]="{ username: ('translation.key' | translate) }">translated-value-global.form.currentpassword.title1-{ username: &apos;translated-value-translation.key-0&apos; }-1</tag>
"
`);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,23 @@
import { passthrough } from '@yeoman/transform';
import { Minimatch } from 'minimatch';

import { createJhiTransformTranslateReplacer, createJhiTransformTranslateStringifyReplacer } from '../../languages/support/index.js';
import {
type JHITranslateConverterOptions,
createJhiTransformTranslateReplacer,
createJhiTransformTranslateStringifyReplacer,
createJhiTranslateReplacer,
escapeTranslationValue,
} from '../../languages/support/index.js';

const PLACEHOLDER_REGEX = /(?:placeholder|title)=['|"](\{\{\s?['|"]([a-zA-Z0-9.\-_]+)['|"]\s?\|\s?translate\s?\}\})['|"]/.source;

const JHI_TRANSLATE_REGEX = /(\n?\s*[a-z][a-zA-Z]*Translate="[a-zA-Z0-9 +{}'_!?.]+")/.source;
const TRANSLATE_VALUES_REGEX = /(\n?\s*\[translateValues\]="\{(?:(?!\}").)*?\}")/.source;
const TRANSLATE_REGEX = [JHI_TRANSLATE_REGEX, TRANSLATE_VALUES_REGEX].join('|');

function getTranslationValue(getWebappTranslation, key, data) {
export type ReplacerOptions = { jhiPrefix: string; enableTranslation: boolean };

function getTranslationValue(getWebappTranslation, key, data?) {
return getWebappTranslation(key, data) || undefined;
}

Expand All @@ -40,9 +48,19 @@ function getTranslationValue(getWebappTranslation, key, data) {
* @param {object} [options]
* @param {number} [options.keyIndex]
* @param {number} [options.replacementIndex]
* @param {any} [options.escape]
* @returns {string}
*/
function replaceTranslationKeysWithText(getWebappTranslation, content, regexSource, { keyIndex = 1, replacementIndex = 1, escape } = {}) {
function replaceTranslationKeysWithText(
getWebappTranslation,
content,
regexSource,
{
keyIndex = 1,
replacementIndex = 1,
escape,
}: { keyIndex?: number; replacementIndex?: number; escape?: (str: string, match: any) => string } = {},
) {
const regex = new RegExp(regexSource, 'g');
const allMatches = content.matchAll(regex);
for (const match of allMatches) {
Expand Down Expand Up @@ -102,15 +120,103 @@ function replaceErrorMessage(getWebappTranslation, content) {
return replaceJSTranslation(getWebappTranslation, content, 'errorMessage');
}

/**
* Convert interpolation values to angular template for later processing.
* Numbers are left as is, strings are wrapped in `{{ }}`.
*/
const translationValueInterpolate = (parsedInterpolate: Record<string, string>): Record<string, string> =>
Object.fromEntries(Object.entries(parsedInterpolate).map(([key, value]) => [key, /\d+/.test(value) ? value : `{{ ${value} }}`]));

/**
* Creates a `jhiTranslate` attribute with optional translateValues.
* Or the translation value if translation is disabled.
*/
const tagTranslation = (
getWebappTranslation: any,
{ enableTranslation, jhiPrefix }: ReplacerOptions,
{ key, parsedInterpolate, prefix, suffix }: JHITranslateConverterOptions,
) => {
const translatedValueInterpolate = parsedInterpolate ? translationValueInterpolate(parsedInterpolate) : undefined;
const translatedValue = escapeTranslationValue(getTranslationValue(getWebappTranslation, key, translatedValueInterpolate));

if (enableTranslation) {
const translateValuesAttr = parsedInterpolate
? ` [translateValues]="{ ${Object.entries(parsedInterpolate)
.map(([key, value]) => `${key}: ${value}`)
.join(', ')} }"`
: '';
return ` ${jhiPrefix}Translate="${key}"${translateValuesAttr}${prefix}${translatedValue}${suffix}`;
}

return `${prefix}${translatedValue}${suffix}`;
};

/**
* Creates a `jhiTranslate` attribute with optional translateValues.
* Or the translation value if translation is disabled.
*/
const tagPipeTranslation = (
getWebappTranslation: any,
{ enableTranslation, jhiPrefix }: ReplacerOptions,
{ key, parsedInterpolate, prefix, suffix }: JHITranslateConverterOptions,
) => {
if (!parsedInterpolate || Object.keys(parsedInterpolate).length === 0) {
throw new Error(`No interpolation values found for translation key ${key}, use __jhiTranslateTag__ instead.`);
}
const translatedValueInterpolate = Object.fromEntries(
Object.entries(parsedInterpolate).map(([key, value]) => [key, getWebappTranslation(value)]),
);
const translatedValue = escapeTranslationValue(getTranslationValue(getWebappTranslation, key, translatedValueInterpolate));
if (enableTranslation) {
const translateValuesAttr = ` [translateValues]="{ ${Object.entries(parsedInterpolate)
.map(([key, value]) => `${key}: ('${value}' | translate)`)
.join(', ')} }"`;
return ` ${jhiPrefix}Translate="${key}"${translateValuesAttr}${prefix}${translatedValue}${suffix}`;
}

return `${prefix}${translatedValue}${suffix}`;
};

/**
* Creates a `translate` pipe.
* Or the translation value if translation is disabled.
*/
const pipeTranslation = (getWebappTranslation: any, { enableTranslation }: ReplacerOptions, { key }: JHITranslateConverterOptions) => {
if (enableTranslation) {
return `{{ '${key}' | translate }}`;
}

return `${escapeTranslationValue(getTranslationValue(getWebappTranslation, key))}`;
};

/**
* Replace and cleanup translations.
*
* @type {import('../generator-base.js').EditFileCallback}
* @this {import('../generator-base.js')}
*/
export const createTranslationReplacer = (getWebappTranslation, enableTranslation) => {
export const createTranslationReplacer = (getWebappTranslation, opts: ReplacerOptions | boolean) => {
const htmlJhiTranslateReplacer = createJhiTransformTranslateReplacer(getWebappTranslation, { escapeHtml: true });
const htmlJhiTranslateStringifyReplacer = createJhiTransformTranslateStringifyReplacer(getWebappTranslation, { escapeHtml: true });
const htmlJhiTranslateStringifyReplacer = createJhiTransformTranslateStringifyReplacer(getWebappTranslation);
let translationReplacer: ((content: string) => string) | undefined;
const enableTranslation = typeof opts === 'boolean' ? opts : opts.enableTranslation;
if (typeof opts !== 'boolean') {
translationReplacer = createJhiTranslateReplacer(
optsReplacer => {
if (optsReplacer.type === 'Tag') {
return tagTranslation(getWebappTranslation, opts, optsReplacer);
}
if (optsReplacer.type === 'TagPipe') {
return tagPipeTranslation(getWebappTranslation, opts, optsReplacer);
}
if (optsReplacer.type === 'Pipe') {
return pipeTranslation(getWebappTranslation, opts, optsReplacer);
}
throw new Error(`Translation type not supported ${optsReplacer.type}`);
},
{ prefixPattern: '>\\s*', suffixPattern: '\\s*<' },
);
}
return function replaceAngularTranslations(content, filePath) {
if (/\.html$/.test(filePath)) {
if (!enableTranslation) {
Expand All @@ -122,6 +228,7 @@ export const createTranslationReplacer = (getWebappTranslation, enableTranslatio
if (/(:?\.html|component\.ts)$/.test(filePath)) {
content = htmlJhiTranslateReplacer(content);
content = htmlJhiTranslateStringifyReplacer(content);
content = translationReplacer?.(content);
}
if (!enableTranslation) {
if (/(:?route|module)\.ts$/.test(filePath)) {
Expand All @@ -138,11 +245,9 @@ export const createTranslationReplacer = (getWebappTranslation, enableTranslatio
const minimatch = new Minimatch('**/*{.html,.component.ts,.route.ts,.module.ts}');
export const isTranslatedAngularFile = file => minimatch.match(file.path);

const translateAngularFilesTransform = (getWebappTranslation, enableTranslation) => {
const translate = createTranslationReplacer(getWebappTranslation, enableTranslation);
export const translateAngularFilesTransform = (getWebappTranslation, opts: ReplacerOptions | boolean) => {
const translate = createTranslationReplacer(getWebappTranslation, opts);
return passthrough(file => {
file.contents = Buffer.from(translate(file.contents.toString(), file.path));
});
};

export default translateAngularFilesTransform;
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<div class="d-flex justify-content-center">
@if (account$ | async; as account) {
<div class="col-md-8">
<h2 <%= jhiPrefix %>Translate="password.title" [translateValues]="{ username: account.login }">__jhiTransformTranslate__('password.title', { "username": "{{ account.login }}" })</h2>
<h2>__jhiTranslateTag__('password.title', { "username": "account.login" })</h2>

@if (success()) {
<div class="alert alert-success" <%= jhiPrefix %>Translate="password.messages.success">__jhiTransformTranslate__('password.messages.success')</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
-%>
<div>
@if (account) {
<h2 id="session-page-heading" <%= jhiPrefix %>Translate="sessions.title" [translateValues]="{ username: account.login }">__jhiTransformTranslate__('sessions.title', { "username": "{{ account.login }}" })</h2>
<h2 id="session-page-heading">__jhiTranslateTag__('sessions.title', { "username": "account.login" })</h2>
}

@if (success) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<div class="d-flex justify-content-center">
<div class="col-md-8">
@if (settingsForm.value.login) {
<h2 <%= jhiPrefix %>Translate="settings.title" [translateValues]="{ username: settingsForm.value.login }">__jhiTransformTranslate__('settings.title', { "username": "{{ settingsForm.value.login }}" })</h2>
<h2>__jhiTranslateTag__('settings.title', { "username": "settingsForm.value.login" })</h2>
}

@if (success()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</div>
<%_ } _%>

<p <%= jhiPrefix %>Translate="logs.nbloggers" [translateValues]="{ total: loggers()?.length }">__jhiTransformTranslate__('logs.nbloggers', { "total": "{{ loggers()?.length }}" })</p>
<p>__jhiTranslateTag__('logs.nbloggers', { "total": "loggers()?.length" })</p>

<span <%= jhiPrefix %>Translate="logs.filter">__jhiTransformTranslate__('logs.filter')</span>
<input type="text" [ngModel]="filter()" (ngModelChange)="filter.set($event)" class="form-control" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<div class="modal-body">
<<%= jhiPrefixDashed %>-alert-error></<%= jhiPrefixDashed %>-alert-error>

<p <%= jhiPrefix %>Translate="userManagement.delete.question" [translateValues]="{ login: user.login }">__jhiTransformTranslate__('userManagement.delete.question', { "login": "{{ user.login }}" })</p>
<p>__jhiTranslateTag__('userManagement.delete.question', { "login": "user.login" })</p>
</div>

<div class="modal-footer">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

<div class="modal-body">
<<%= jhiPrefixDashed %>-alert-error></<%= jhiPrefixDashed %>-alert-error>
<p id="<%= jhiPrefixDashed %>-delete-<%= entityInstance %>-heading" <%= jhiPrefix %>Translate="<%= i18nKeyPrefix %>.delete.question" [translateValues]="{ id: <%= entityInstance %>.<%= primaryKey.name %> }">__jhiTransformTranslate__('<%- i18nKeyPrefix %>.delete.question', { "id": "{{ <%- entityInstance%>.<%- primaryKey.name%> }}" })</p>
<p id="<%= jhiPrefixDashed %>-delete-<%= entityInstance %>-heading">__jhiTranslateTag__('<%- i18nKeyPrefix %>.delete.question', { "id": "<%- entityInstance%>.<%- primaryKey.name%>" })</p>
</div>

<div class="modal-footer">
Expand Down
Loading

0 comments on commit 9147e93

Please sign in to comment.