Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ <h4 class="card-title" [innerHTML]="dsoTitle"></h4>
</p>
<p *ngIf="dso.hasMetadata('dc.description')" class="item-description card-text">
<ds-truncatable-part [id]="dso.id" [minLines]="3">
<span [innerHTML]="firstMetadataValue('dc.description')" [attr.lang]="dso.firstMetadata('dc.description')?.language || null"></span>
<span [innerHTML]="firstMetadataValue('dc.description')" [attr.lang]="dso.firstMetadata('dc.description')?.language | dsLang"></span>
</ds-truncatable-part>
</p>
<div *ngIf="linkType != linkTypes.None" class="text-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ <h4 class="card-title" [innerHTML]="dsoTitle"></h4>
</p>
<p *ngIf="dso.hasMetadata('dc.description')" class="item-description card-text">
<ds-truncatable-part [id]="dso.id" [minLines]="3">
<span [innerHTML]="firstMetadataValue('dc.description')" [attr.lang]="dso.firstMetadata('dc.description')?.language || null"></span>
<span [innerHTML]="firstMetadataValue('dc.description')" [attr.lang]="dso.firstMetadata('dc.description')?.language | dsLang"></span>
</ds-truncatable-part>
</p>
<div *ngIf="linkType != linkTypes.None" class="text-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ <h4 class="card-title" [innerHTML]="dsoTitle"></h4>
</ds-truncatable-part>
<p *ngIf="dso.hasMetadata('dc.description')" class="item-description card-text text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="3">
<span [innerHTML]="firstMetadataValue('dc.description')" [attr.lang]="dso.firstMetadata('dc.description')?.language || null"></span>
<span [innerHTML]="firstMetadataValue('dc.description')" [attr.lang]="dso.firstMetadata('dc.description')?.language | dsLang"></span>
</ds-truncatable-part>
</p>
<div *ngIf="linkType != linkTypes.None" class="text-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<span *ngIf="dso.allMetadata(['dc.description']).length > 0"
class="item-list-org-unit-description">
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span
[innerHTML]="firstMetadataValue('dc.description')" [attr.lang]="dso.firstMetadata('dc.description')?.language || null"></span>
[innerHTML]="firstMetadataValue('dc.description')" [attr.lang]="dso.firstMetadata('dc.description')?.language | dsLang"></span>
</ds-truncatable-part>
</span>
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { of as observableOf } from 'rxjs';
import { OrgUnitSearchResultListElementComponent } from './org-unit-search-result-list-element.component';
import { Item } from '../../../../../core/shared/item.model';
import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
import { DsLangPipe } from '../../../../../shared/utils/ds-lang.pipe';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
Expand Down Expand Up @@ -75,7 +76,7 @@ describe('OrgUnitSearchResultListElementComponent', () => {
}
}
)],
declarations: [ OrgUnitSearchResultListElementComponent , TruncatePipe],
declarations: [ OrgUnitSearchResultListElementComponent , TruncatePipe, DsLangPipe],
providers: [
{ provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock },
Expand Down Expand Up @@ -145,7 +146,7 @@ describe('OrgUnitSearchResultListElementComponent', () => {
}
}
)],
declarations: [OrgUnitSearchResultListElementComponent, TruncatePipe],
declarations: [OrgUnitSearchResultListElementComponent, TruncatePipe, DsLangPipe],
providers: [
{provide: TruncatableService, useValue: {}},
{provide: DSONameService, useClass: DSONameServiceMock},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ItemSearchResult } from '../../../../../shared/object-collection/shared
import { Component } from '@angular/core';
import { SidebarSearchListElementComponent } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component';
import { Item } from '../../../../../core/shared/item.model';
import { metadataLangToBcp47 } from '../../../../../shared/utils/metadata-language.util';

@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.SideBarSearchModal)
@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.SideBarSearchModalCurrent)
Expand All @@ -29,6 +30,6 @@ export class OrgUnitSidebarSearchListElementComponent extends SidebarSearchListE
* Get the language of the Org Unit description.
*/
getDescriptionLang(): string | null {
return this.dso.firstMetadata('dc.description')?.language || null;
return metadataLangToBcp47(this.dso.firstMetadata('dc.description')?.language);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<ng-template #descTemplate>
<span class="text-muted">
<span class="item-list-job-title">
<span [innerHTML]="mdRepresentation.firstMetadataValue(['dc.description'])" [attr.lang]="mdRepresentation.firstMetadata(['dc.description'])?.language || null"></span>
<span [innerHTML]="mdRepresentation.firstMetadataValue(['dc.description'])" [attr.lang]="mdRepresentation.firstMetadata(['dc.description'])?.language | dsLang"></span>
</span>
</span>
</ng-template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-rep
import { OrgUnitItemMetadataListElementComponent } from './org-unit-item-metadata-list-element.component';
import { Item } from '../../../../core/shared/item.model';
import { MetadataValue } from '../../../../core/shared/metadata.models';
import { DsLangPipe } from '../../../../shared/utils/ds-lang.pipe';

const description = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.';
const organisation = 'Anonymous';
Expand All @@ -24,7 +25,7 @@ describe('OrgUnitItemMetadataListElementComponent', () => {
imports:[
NgbModule
],
declarations: [OrgUnitItemMetadataListElementComponent],
declarations: [OrgUnitItemMetadataListElementComponent, DsLangPipe],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(OrgUnitItemMetadataListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Choose a template. Priority: markdown, link, browse link.
-->
<ng-container *ngTemplateOutlet="(renderMarkdown ? markdown : (hasLink(mdValue) ? link : (hasBrowseDefinition() ? browselink : simple)));
context: {value: mdValue.value, lang: mdValue.language || null}">
context: {value: mdValue.value, lang: getLang(mdValue)}">
</ng-container>
<span class="separator" *ngIf="!last" [innerHTML]="separator"></span>
</ng-container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ describe('MetadataValuesComponent', () => {
}
});

it('should render the metadata language as a normalized BCP 47 lang attribute', () => {
const langEls = fixture.debugElement.queryAll(By.css('[lang]'));
expect(langEls.length).toBeGreaterThan(0);
langEls.forEach((el) => {
expect(el.nativeElement.getAttribute('lang')).toEqual('en-US');
});
expect(fixture.nativeElement.innerHTML).not.toContain('lang="en_US"');
});

it('should contain separators equal to the amount of metadata values minus one', () => {
const separators = fixture.debugElement.queryAll(By.css('span.separator'));
expect(separators.length).toBe(mockMetadata.length - 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BrowseDefinition } from '../../../core/shared/browse-definition.model';
import { hasValue } from '../../../shared/empty.util';
import { VALUE_LIST_BROWSE_DEFINITION } from '../../../core/shared/value-list-browse-definition.resource-type';
import { environment } from '../../../../environments/environment';
import { metadataLangToBcp47 } from '../../../shared/utils/metadata-language.util';

/**
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
Expand Down Expand Up @@ -112,4 +113,13 @@ export class MetadataValuesComponent implements OnChanges {
return { target: '_blank', rel: 'noopener noreferrer' };
}
}

/**
* Normalize a metadata value's language into a valid BCP 47 tag for the HTML
* `lang` attribute, or null when none applies.
* @param value A MetadataValue being displayed
*/
getLang(value: MetadataValue): string | null {
return metadataLangToBcp47(value.language);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ <h3 class="h5 simple-view-element-header">{{"item.page.filesection.original.bund

<ng-container *ngIf="file.hasMetadata('dc.description')">
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
<dd class="col-md-8" [attr.lang]="file.firstMetadata('dc.description')?.language || null">{{file.firstMetadataValue("dc.description")}}</dd>
<dd class="col-md-8" [attr.lang]="file.firstMetadata('dc.description')?.language | dsLang">{{file.firstMetadataValue("dc.description")}}</dd>
</ng-container>
</dl>
</div>
Expand Down Expand Up @@ -69,7 +69,7 @@ <h3 class="h5 simple-view-element-header">{{"item.page.filesection.license.bundl


<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
<dd class="col-md-8" [attr.lang]="file.firstMetadata('dc.description')?.language || null">{{file.firstMetadataValue("dc.description")}}</dd>
<dd class="col-md-8" [attr.lang]="file.firstMetadata('dc.description')?.language | dsLang">{{file.firstMetadataValue("dc.description")}}</dd>
</dl>
</div>
<div class="col-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.m
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { VarDirective } from '../../../../shared/utils/var.directive';
import { FileSizePipe } from '../../../../shared/utils/file-size-pipe';
import { DsLangPipe } from '../../../../shared/utils/ds-lang.pipe';
import { MetadataFieldWrapperComponent } from '../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { NO_ERRORS_SCHEMA } from '@angular/core';
Expand Down Expand Up @@ -67,7 +68,7 @@ describe('FullFileSectionComponent', () => {
useClass: TranslateLoaderMock
}
}), BrowserAnimationsModule],
declarations: [FullFileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent],
declarations: [FullFileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent, DsLangPipe],
providers: [
{ provide: BitstreamDataService, useValue: bitstreamDataService },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, Input, OnInit } from '@angular/core';
import { Item } from '../../../../core/shared/item.model';
import { makeLinks } from '../../../../shared/clarin-shared-util';
import { metadataLangToBcp47 } from '../../../../shared/utils/metadata-language.util';

@Component({
selector: 'ds-clarin-description-item-field',
Expand All @@ -27,7 +28,7 @@ export class ClarinDescriptionItemFieldComponent implements OnInit {
ngOnInit(): void {
this.descriptionEntries = this.item.allMetadata(this.fields).map(md => ({
value: makeLinks(md.value),
language: md.language || null
language: metadataLangToBcp47(md.language)
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { LicenseType } from '../../item-page/clarin-license-info/clarin-license-
import { ListableObject } from '../object-collection/shared/listable-object.model';
import { ItemSearchResult } from '../object-collection/shared/item-search-result.model';
import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
import { metadataLangToBcp47 } from '../utils/metadata-language.util';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { ClarinDateService } from '../clarin-date.service';
import { AUTHOR_METADATA_FIELDS } from '../../core/shared/clarin/constants';
Expand Down Expand Up @@ -151,7 +152,7 @@ export class ClarinItemBoxViewComponent implements OnInit {
this.itemUri = getItemPageRoute(this.item);
const descMeta = this.item?.firstMetadata('dc.description');
this.itemDescription = descMeta?.value || null;
this.itemDescriptionLang = descMeta?.language || null;
this.itemDescriptionLang = metadataLangToBcp47(descMeta?.language);
this.itemPublisher = this.item?.firstMetadataValue('dc.publisher');
this.itemDate = this.clarinDateService.composeItemDate(this.item);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ItemSearchResult } from '../../../../object-collection/shared/item-sear
import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils';
import { TruncatableService } from '../../../../truncatable/truncatable.service';
import { TruncatePipe } from '../../../../utils/truncate.pipe';
import { DsLangPipe } from '../../../../utils/ds-lang.pipe';
import { ItemSearchResultGridElementComponent } from './item-search-result-grid-element.component';

const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult();
Expand Down Expand Up @@ -218,7 +219,7 @@ export function getEntityGridElementTestComponent(component, searchResultWithMet
NoopAnimationsModule,
TranslateModule.forRoot()
],
declarations: [component, TruncatePipe],
declarations: [component, TruncatePipe, DsLangPipe],
providers: [
{ provide: TruncatableService, useValue: truncatableServiceStub },
{ provide: ObjectCacheService, useValue: {} },
Expand Down
4 changes: 3 additions & 1 deletion src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ import { ReplacePipe } from './utils/replace.pipe';
import { ClarinDateService } from './clarin-date.service';
import { ItemIdentifierService } from './item-identifier.service';
import { ClarinLicenseRequiredInfoCheckedPipe } from './utils/clarin-license-required-info-checked.pipe';
import { DsLangPipe } from './utils/ds-lang.pipe';

const MODULES = [
CommonModule,
Expand Down Expand Up @@ -347,7 +348,8 @@ const PIPES = [
ClarinLicenseRequiredInfoPipe,
CharToEndPipe,
ClarinSafeHtmlPipe,
ReplacePipe
ReplacePipe,
DsLangPipe
];

const COMPONENTS = [
Expand Down
25 changes: 25 additions & 0 deletions src/app/shared/utils/ds-lang.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { DsLangPipe } from './ds-lang.pipe';

describe('DsLangPipe', () => {
let pipe: DsLangPipe;

beforeEach(() => {
pipe = new DsLangPipe();
});

it('should create', () => {
expect(pipe).toBeTruthy();
});

it('should normalize a Java-style locale to a BCP 47 tag', () => {
expect(pipe.transform('en_US')).toEqual('en-US');
expect(pipe.transform('cs_CZ')).toEqual('cs-CZ');
});

it('should return null for the wildcard, empty and nullish values', () => {
expect(pipe.transform('*')).toBeNull();
expect(pipe.transform('')).toBeNull();
expect(pipe.transform(null)).toBeNull();
expect(pipe.transform(undefined)).toBeNull();
});
});
24 changes: 24 additions & 0 deletions src/app/shared/utils/ds-lang.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Pipe, PipeTransform } from '@angular/core';

import { metadataLangToBcp47 } from './metadata-language.util';

/**
* Pipe that normalizes a DSpace metadata language value into a valid BCP 47
* language tag for an HTML `lang` attribute, or `null` when none applies.
*
* Delegates to {@link metadataLangToBcp47}, so binding `[attr.lang]` through this
* pipe both omits the attribute for absent/wildcard languages and converts
* Java-style locales (`en_US` → `en-US`).
*/
@Pipe({
name: 'dsLang'
})
export class DsLangPipe implements PipeTransform {
/**
* @param {string} language the raw metadata language value
* @returns {string | null} a BCP 47 language tag, or null when none applies
*/
transform(language: string | null | undefined): string | null {
return metadataLangToBcp47(language);
}
}
30 changes: 30 additions & 0 deletions src/app/shared/utils/metadata-language.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { metadataLangToBcp47 } from './metadata-language.util';

describe('metadataLangToBcp47', () => {
it('should return null for nullish values', () => {
expect(metadataLangToBcp47(null)).toBeNull();
expect(metadataLangToBcp47(undefined)).toBeNull();
});

it('should return null for an empty string', () => {
expect(metadataLangToBcp47('')).toBeNull();
});

it('should return null for the wildcard language', () => {
expect(metadataLangToBcp47('*')).toBeNull();
});

it('should convert Java-style locale separators to BCP 47 hyphens', () => {
expect(metadataLangToBcp47('en_US')).toEqual('en-US');
expect(metadataLangToBcp47('cs_CZ')).toEqual('cs-CZ');
expect(metadataLangToBcp47('de_DE')).toEqual('de-DE');
});

it('should leave a bare language subtag unchanged', () => {
expect(metadataLangToBcp47('en')).toEqual('en');
});

it('should leave already-hyphenated tags unchanged', () => {
expect(metadataLangToBcp47('en-US')).toEqual('en-US');
});
});
20 changes: 20 additions & 0 deletions src/app/shared/utils/metadata-language.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Convert a DSpace metadata language value into a valid BCP 47 language tag
* suitable for an HTML `lang` attribute, or `null` when none applies.
*
* DSpace stores the metadata language as a Java-style locale (e.g. `en_US`,
* `cs_CZ`), the wildcard `*` (language-independent), or nothing. None of the
* underscore forms or the wildcard are valid BCP 47 / HTML `lang` values, so
* assistive technology ignores them. This normalizes them:
* - `null`/`undefined`/empty and the wildcard `*` → `null` (attribute omitted)
* - the Java-style separator is converted: `en_US` → `en-US`, `cs_CZ` → `cs-CZ`
*
* @param language the raw metadata language value
* @returns a BCP 47 language tag, or `null` when no valid tag applies
*/
export function metadataLangToBcp47(language: string | null | undefined): string | null {
if (!language || language === '*') {
return null;
}
return language.replace(/_/g, '-');
}
Loading