Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add simplified and improved translation process. #25650

Merged
merged 3 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading