diff --git a/core-web/libs/data-access/src/lib/dot-languages/dot-languages.service.spec.ts b/core-web/libs/data-access/src/lib/dot-languages/dot-languages.service.spec.ts index 96c4c1006008..32f6a3d2e840 100644 --- a/core-web/libs/data-access/src/lib/dot-languages/dot-languages.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-languages/dot-languages.service.spec.ts @@ -1,6 +1,11 @@ import { createHttpFactory, HttpMethod, SpectatorHttp } from '@ngneat/spectator/jest'; -import { DotLanguagesService } from '@dotcms/data-access'; +import { + DotLanguagesService, + LANGUAGE_API_URL, + LANGUAGE_API_URL_WITH_VARS +} from '@dotcms/data-access'; +import { DotLanguage } from '@dotcms/dotcms-models'; describe('DotLanguagesService', () => { let spectator: SpectatorHttp; @@ -10,13 +15,16 @@ describe('DotLanguagesService', () => { it('should get Languages', () => { spectator.service.get().subscribe(); - spectator.expectOne(`/api/v2/languages`, HttpMethod.GET); + spectator.expectOne(LANGUAGE_API_URL_WITH_VARS, HttpMethod.GET); }); it('should get Languages by content indode', () => { const contentInode = '2'; spectator.service.get(contentInode).subscribe(); - spectator.expectOne(`/api/v2/languages?contentInode=${contentInode}`, HttpMethod.GET); + spectator.expectOne( + `${LANGUAGE_API_URL_WITH_VARS}&contentInode=${contentInode}`, + HttpMethod.GET + ); }); it('should get Languages by pageId', () => { @@ -24,4 +32,48 @@ describe('DotLanguagesService', () => { spectator.service.getLanguagesUsedPage(pageIdentifier).subscribe(); spectator.expectOne(`/api/v1/page/${pageIdentifier}/languages`, HttpMethod.GET); }); + + it('should add a new language', () => { + const language = { + languageCode: 'fr', + countryCode: 'FR', + language: 'French', + country: 'France' + }; + spectator.service.add(language).subscribe(); + const req = spectator.expectOne(LANGUAGE_API_URL, HttpMethod.POST); + expect(req.request.body).toEqual(language); + }); + + it('should get a language by id', () => { + const id = 1; + spectator.service.getById(id).subscribe(); + spectator.expectOne(`${LANGUAGE_API_URL}/id/${id}`, HttpMethod.GET); + }); + + it('should update a language', () => { + const language = { + id: '1', + languageCode: 'fr' + } as unknown as DotLanguage; + spectator.service.update(language).subscribe(); + spectator.expectOne(`${LANGUAGE_API_URL}/${language.id}`, HttpMethod.PUT); + }); + + it('should delete a language by id', () => { + const id = 1; + spectator.service.delete(id).subscribe(); + spectator.expectOne(`${LANGUAGE_API_URL}/${id}`, HttpMethod.DELETE); + }); + + it('should make a language the default language', () => { + const id = 1; + spectator.service.makeDefault(id).subscribe(); + spectator.expectOne(`${LANGUAGE_API_URL}/${id}/_makedefault`, HttpMethod.PUT); + }); + + it('should get languages and countries in ISO format', () => { + spectator.service.getISO().subscribe(); + spectator.expectOne(`${LANGUAGE_API_URL}/iso`, HttpMethod.GET); + }); }); diff --git a/core-web/libs/data-access/src/lib/dot-languages/dot-languages.service.ts b/core-web/libs/data-access/src/lib/dot-languages/dot-languages.service.ts index 77ba037f8b37..db337b7bebe2 100644 --- a/core-web/libs/data-access/src/lib/dot-languages/dot-languages.service.ts +++ b/core-web/libs/data-access/src/lib/dot-languages/dot-languages.service.ts @@ -5,7 +5,11 @@ import { inject, Injectable } from '@angular/core'; import { pluck } from 'rxjs/operators'; -import { DotLanguage } from '@dotcms/dotcms-models'; +import { DotAddLanguage, DotLanguage, DotLanguagesISO } from '@dotcms/dotcms-models'; + +export const LANGUAGE_API_URL = '/api/v2/languages'; + +export const LANGUAGE_API_URL_WITH_VARS = '/api/v2/languages?countLangVars=true'; /** * Provide util methods to get Languages available in the system. @@ -23,8 +27,8 @@ export class DotLanguagesService { */ get(contentInode?: string): Observable { const url = !contentInode - ? '/api/v2/languages' - : `/api/v2/languages?contentInode=${contentInode}`; + ? LANGUAGE_API_URL_WITH_VARS + : `${LANGUAGE_API_URL_WITH_VARS}&contentInode=${contentInode}`; return this.httpClient.get(url).pipe(pluck('entity')); } @@ -40,4 +44,62 @@ export class DotLanguagesService { .get(`/api/v1/page/${pageIdentifier}/languages`) .pipe(pluck('entity')); } + + /** + * Add a new language to the system. + * + * @param {DotAddLanguage} language - The language to be added. + * @return {Observable} An observable of the language added. + */ + add(language: DotAddLanguage): Observable { + return this.httpClient.post(LANGUAGE_API_URL, language).pipe(pluck('entity')); + } + + /** + * Get Language by id. + * + * @param {number} id + * @return {Observable} + */ + getById(id: number): Observable { + return this.httpClient.get(`${LANGUAGE_API_URL}/id/${id}`).pipe(pluck('entity')); + } + + /** + * Update a language. + * + * @param {DotLanguage} language - The language to be updated. + * @return {Observable} An observable of the updated language. + */ + update(language: DotLanguage): Observable { + return this.httpClient + .put(`${LANGUAGE_API_URL}/${language.id}`, language) + .pipe(pluck('entity')); + } + + /** + * Delete a language. + * + * @param {string} id - + * @return {Observable} + */ + delete(id: number): Observable { + return this.httpClient.delete(`${LANGUAGE_API_URL}/${id}`).pipe(pluck('entity')); + } + + /** + * Make a language the default language. + * + * @param {number} id - The identifier of the language to be made the default. + * @return {Observable} + */ + makeDefault(id: number): Observable { + return this.httpClient + .put(`${LANGUAGE_API_URL}/${id}/_makedefault`, {}) + .pipe(pluck('entity')); + } + + getISO(): Observable { + return this.httpClient.get(`${LANGUAGE_API_URL}/iso`).pipe(pluck('entity')); + } } diff --git a/core-web/libs/dotcms-models/src/lib/dot-language.model.ts b/core-web/libs/dotcms-models/src/lib/dot-language.model.ts index 8f5c4ad3efd1..9f08e6d4efce 100644 --- a/core-web/libs/dotcms-models/src/lib/dot-language.model.ts +++ b/core-web/libs/dotcms-models/src/lib/dot-language.model.ts @@ -1,10 +1,24 @@ -export interface DotLanguage { - id: number; - languageCode: string; +export interface DotAddLanguage { + country: string; countryCode: string; language: string; - country: string; + languageCode: string; +} + +export interface DotLanguage extends DotAddLanguage { + id: number; defaultLanguage?: boolean; translated?: boolean; isoCode?: string; + variables?: { count: number; total: number }; +} + +export interface DotISOItem { + code: string; + name: string; +} + +export interface DotLanguagesISO { + countries: DotISOItem[]; + languages: DotISOItem[]; } diff --git a/core-web/libs/portlets/dot-locales/data-access/src/index.ts b/core-web/libs/portlets/dot-locales/data-access/src/index.ts index eb7235986dd8..e69de29bb2d1 100644 --- a/core-web/libs/portlets/dot-locales/data-access/src/index.ts +++ b/core-web/libs/portlets/dot-locales/data-access/src/index.ts @@ -1 +0,0 @@ -export * from './lib/resolvers/dot-locales-list.resolver'; diff --git a/core-web/libs/portlets/dot-locales/data-access/src/lib/resolvers/dot-locales-list.resolver.spec.ts b/core-web/libs/portlets/dot-locales/data-access/src/lib/resolvers/dot-locales-list.resolver.spec.ts deleted file mode 100644 index 4f3fbad9f0aa..000000000000 --- a/core-web/libs/portlets/dot-locales/data-access/src/lib/resolvers/dot-locales-list.resolver.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Observable, of } from 'rxjs'; - -import { EnvironmentInjector, runInInjectionContext } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; - -import { DotLanguagesService } from '@dotcms/data-access'; -import { DotLanguage } from '@dotcms/dotcms-models'; - -import { DotLocalesListResolver } from './dot-locales-list.resolver'; - -const mockLanguages: DotLanguage[] = [ - { - id: 1, - languageCode: 'en', - countryCode: 'US', - language: 'English', - country: 'United States', - isoCode: 'en-US', - defaultLanguage: true - }, - { - id: 2, - languageCode: 'es', - countryCode: 'ES', - language: 'Spanish', - country: 'Spain', - isoCode: 'es-ES', - defaultLanguage: false - } -]; - -describe('DotLocalesListResolver', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - { - provide: DotLanguagesService, - useValue: { - get: () => of(mockLanguages) - } - } - ] - }); - }); - - it('should resolve languages list', (done) => { - const result = runInInjectionContext(TestBed.inject(EnvironmentInjector), () => - DotLocalesListResolver(null, null) - ); - - (result as Observable).subscribe((languages) => { - expect(languages).toEqual(mockLanguages); - done(); - }); - }); -}); diff --git a/core-web/libs/portlets/dot-locales/data-access/src/lib/resolvers/dot-locales-list.resolver.ts b/core-web/libs/portlets/dot-locales/data-access/src/lib/resolvers/dot-locales-list.resolver.ts deleted file mode 100644 index 6b3fcfc31fda..000000000000 --- a/core-web/libs/portlets/dot-locales/data-access/src/lib/resolvers/dot-locales-list.resolver.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Observable } from 'rxjs'; - -import { inject } from '@angular/core'; -import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router'; - -import { DotLanguagesService } from '@dotcms/data-access'; -import { DotLanguage } from '@dotcms/dotcms-models'; - -export const DotLocalesListResolver: ResolveFn = ( - _route: ActivatedRouteSnapshot, - _state: RouterStateSnapshot -): Observable => { - const languageService = inject(DotLanguagesService); - - return languageService.get(); -}; diff --git a/core-web/libs/portlets/dot-locales/portlet/src/lib/dot-locales-list/dot-locales-list.component.html b/core-web/libs/portlets/dot-locales/portlet/src/lib/dot-locales-list/dot-locales-list.component.html index 6917f5129f49..9122792fcf7a 100644 --- a/core-web/libs/portlets/dot-locales/portlet/src/lib/dot-locales-list/dot-locales-list.component.html +++ b/core-web/libs/portlets/dot-locales/portlet/src/lib/dot-locales-list/dot-locales-list.component.html @@ -6,6 +6,7 @@ class="locales-list__search" (input)="dt.filterGlobal($event.target.value, 'contains')" type="text" + data-testId="input-search" pInputText /> + + diff --git a/core-web/libs/portlets/dot-locales/portlet/src/lib/share/ui/DotLocaleConfirmationDialog/DotLocaleConfirmationDialog.component.scss b/core-web/libs/portlets/dot-locales/portlet/src/lib/share/ui/DotLocaleConfirmationDialog/DotLocaleConfirmationDialog.component.scss new file mode 100644 index 000000000000..122c9acb3f0f --- /dev/null +++ b/core-web/libs/portlets/dot-locales/portlet/src/lib/share/ui/DotLocaleConfirmationDialog/DotLocaleConfirmationDialog.component.scss @@ -0,0 +1,35 @@ +@use "variables" as *; + +:host { + display: flex; + flex-direction: column; +} + +.locale-confirmation__message { + display: flex; + gap: $spacing-2; + word-break: break-word; + align-items: flex-start; + + i { + color: $color-alert-yellow; + font-size: $font-size-lmd; + } +} + +.locale-confirmation__confirmation { + display: flex; + gap: $spacing-1; + margin: $spacing-4 0; + align-items: center; + + input { + flex: 1; + } +} + +.locale-confirmation__buttons { + display: flex; + justify-content: flex-end; + gap: $spacing-1; +} diff --git a/core-web/libs/portlets/dot-locales/portlet/src/lib/share/ui/DotLocaleConfirmationDialog/DotLocaleConfirmationDialog.component.spec.ts b/core-web/libs/portlets/dot-locales/portlet/src/lib/share/ui/DotLocaleConfirmationDialog/DotLocaleConfirmationDialog.component.spec.ts new file mode 100644 index 000000000000..4a981643966d --- /dev/null +++ b/core-web/libs/portlets/dot-locales/portlet/src/lib/share/ui/DotLocaleConfirmationDialog/DotLocaleConfirmationDialog.component.spec.ts @@ -0,0 +1,81 @@ +import { Spectator, createComponentFactory } from '@ngneat/spectator'; +import { byTestId } from '@ngneat/spectator/jest'; + +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { DotMessageService } from '@dotcms/data-access'; +import { DotLanguage } from '@dotcms/dotcms-models'; +import { DotMessagePipe, MockDotMessageService } from '@dotcms/utils-testing'; + +import { DotLocaleConfirmationDialogComponent } from './DotLocaleConfirmationDialog.component'; + +const messageServiceMock = new MockDotMessageService({ + Cancel: 'Cancel' +}); + +describe('DotLocaleConfirmationDialogComponent', () => { + let spectator: Spectator; + const createComponent = createComponentFactory({ + component: DotLocaleConfirmationDialogComponent, + imports: [DotMessagePipe], + providers: [ + DynamicDialogRef, + { + provide: DynamicDialogConfig, + useValue: { + data: { + acceptLabel: '', + icon: '', + ISOCode: '', + locale: {} as DotLanguage, + message: '' + } + } + }, + { + provide: DotMessageService, + useValue: messageServiceMock + } + ] + }); + beforeEach(() => (spectator = createComponent())); + + it('should disable the confirm button if input value is different from ISOCode', () => { + spectator.component.data.ISOCode = 'en-us'; + spectator.detectChanges(); + + const inputElement = spectator.query(byTestId('input')); + + spectator.typeInElement('fr-ca', inputElement); + + const acceptButton = spectator.query(byTestId('confirm-button')); + expect(acceptButton.disabled).toEqual(true); + }); + + it('should enable the confirm button if input value is same as ISOCode', () => { + jest.spyOn(spectator.component.ref, 'close'); + spectator.component.data.ISOCode = 'en-us'; + spectator.detectChanges(); + + const inputElement = spectator.query(byTestId('input')); + spectator.typeInElement('en-us', inputElement); + + const buttonElement = spectator.query(byTestId('confirm-button')); + + spectator.click(buttonElement); + + expect(buttonElement.disabled).toEqual(false); + expect(spectator.component.ref.close).toHaveBeenCalledWith(true); + }); + + it('should close the dialog without confirmation when cancel button is clicked', () => { + jest.spyOn(spectator.component.ref, 'close'); + spectator.detectChanges(); + + const cancelButton = spectator.query(byTestId('cancel-button')); + + spectator.click(cancelButton); + + expect(spectator.component.ref.close).toHaveBeenCalledWith(false); + }); +}); diff --git a/core-web/libs/portlets/dot-locales/portlet/src/lib/share/ui/DotLocaleConfirmationDialog/DotLocaleConfirmationDialog.component.ts b/core-web/libs/portlets/dot-locales/portlet/src/lib/share/ui/DotLocaleConfirmationDialog/DotLocaleConfirmationDialog.component.ts new file mode 100644 index 000000000000..8340063e35d5 --- /dev/null +++ b/core-web/libs/portlets/dot-locales/portlet/src/lib/share/ui/DotLocaleConfirmationDialog/DotLocaleConfirmationDialog.component.ts @@ -0,0 +1,40 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; + +import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { InputTextModule } from 'primeng/inputtext'; + +import { DotLanguage } from '@dotcms/dotcms-models'; +import { DotMessagePipe } from '@dotcms/ui'; + +export interface DotLocaleConfirmationDialogData { + acceptLabel: string; + icon: string; + ISOCode: string; + locale: DotLanguage; + message: string; +} + +@Component({ + selector: 'dot-locale-confirmation-dialog', + standalone: true, + imports: [CommonModule, DialogModule, ButtonModule, DotMessagePipe, InputTextModule], + templateUrl: './DotLocaleConfirmationDialog.component.html', + styleUrl: './DotLocaleConfirmationDialog.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DotLocaleConfirmationDialogComponent { + readonly config: DynamicDialogConfig = + inject(DynamicDialogConfig); + ref = inject(DynamicDialogRef); + + data: DotLocaleConfirmationDialogData = this.config.data || { + acceptLabel: '', + icon: '', + ISOCode: '', + locale: {} as DotLanguage, + message: '' + }; +} diff --git a/core-web/libs/portlets/dot-locales/portlet/src/lib/share/utils.ts b/core-web/libs/portlets/dot-locales/portlet/src/lib/share/utils.ts new file mode 100644 index 000000000000..98db531ae242 --- /dev/null +++ b/core-web/libs/portlets/dot-locales/portlet/src/lib/share/utils.ts @@ -0,0 +1,9 @@ +import { DotLanguage } from '@dotcms/dotcms-models'; + +export const getLocaleISOCode = (locale: DotLanguage): string => { + if (locale) { + return `${locale.languageCode}${locale.countryCode ? `-${locale.countryCode}` : ''}`; + } + + return ''; +}; diff --git a/core-web/libs/utils-testing/src/lib/dot-language.mock.ts b/core-web/libs/utils-testing/src/lib/dot-language.mock.ts index b073d7bdc954..bfff0abea473 100644 --- a/core-web/libs/utils-testing/src/lib/dot-language.mock.ts +++ b/core-web/libs/utils-testing/src/lib/dot-language.mock.ts @@ -1,4 +1,4 @@ -import { DotLanguage } from '@dotcms/dotcms-models'; +import { DotLanguage, DotLanguagesISO } from '@dotcms/dotcms-models'; export const mockDotLanguage: DotLanguage = { id: 1, @@ -20,3 +20,37 @@ export const mockLanguageArray: DotLanguage[] = [ mockDotLanguage, mockDotLanguageWithoutCountryCode ]; + +export const mockLocales: DotLanguage[] = [ + { + id: 1, + languageCode: 'en', + countryCode: 'US', + language: 'English', + country: 'United States', + isoCode: 'en-US', + defaultLanguage: true, + variables: { count: 1, total: 5 } + }, + { + id: 2, + languageCode: 'es', + countryCode: 'ES', + language: 'Spanish', + country: 'Spain', + isoCode: 'es-ES', + defaultLanguage: false, + variables: { count: 1, total: 1 } + } +]; + +export const mockLanguagesISO: DotLanguagesISO = { + countries: [ + { code: 'US', name: 'United States' }, + { code: 'CA', name: 'Canada' } + ], + languages: [ + { code: 'en', name: 'English' }, + { code: 'es', name: 'Spanish' } + ] +}; diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index c74d323920bf..6710c2d7d330 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -5676,11 +5676,31 @@ block-editor.extension.ai.confirmation.header=Discard Changes? block-editor.extension.ai.confirmation.message=It looks like you've made significant or complex edits. Are you sure you want to discard these changes? Any unsaved progress will be lost. locales.add.locale=Add Locale +locales.edit.locale=Edit Locale locales.locale=Locale locales.language=Language locales.country=Country locales.variables=Variables +locales.edit.locale=Edit Locale +locales.add.locale=Add Locale locales.menu=Menu +locales.edit=Edit Locale +locales.push.publish=Push Publish +locales.delete=Delete +locales.set.as.default=Set As Default +locales.language.input.placeholder=Select form list or type +locales.language.hint=Select the language you want to add from the ISO language list. +locales.country.hint=If you wish to add a specific location for the language select from the list. +locale.set.default.confirmation.title=Are you sure you want to set ${0} as the default? +locale.set.default.confirmation.message=Setting this language as the default will alter the language of all site content and communications.

Be mindful of any unintended consequences. +locale.set.default.confirmation.accept.button=Set as Default +locale.delete.confirmation.title=Are you sure you want to delete {0} locale? +locale.delete.confirmation.message=Attention: This action will permanently remove the selected locale and all associated content items from your site.

Please review your decision carefully before proceeding. +locale.delete.confirmation.notification.title=Locale Deletion In Progress +locale.delete.confirmation.notification.message=Removal process running, it may take some time. +locale.action.confirmation.text=Type {0} to confirm +locale.notification.success.title=Changes Saved +locale.notification.success.message=Your changes have been saved. editpage.language-change-missing-lang-populate.confirm.header=Create New Language Version? editpage.language-change-missing-lang-populate.confirm.message=Page does not exist in the selected language. Create a new version in {0}?