From eae5b950cc19e19ab81a4ddda4a1d550cd4b322b Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 11 Sep 2024 18:16:01 -0400 Subject: [PATCH] Add utils for ranking and finding labels --- CHANGELOG.md | 17 ++++ .../src/arches_vue_utils/constants.ts | 3 + .../src/arches_vue_utils/declarations.test.ts | 1 - .../src/arches_vue_utils/types.ts | 18 ++++ .../src/arches_vue_utils/utils.spec.ts | 88 +++++++++++++++++++ .../src/arches_vue_utils/utils.ts | 58 ++++++++++++ 6 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 arches_vue_utils/src/arches_vue_utils/constants.ts delete mode 100644 arches_vue_utils/src/arches_vue_utils/declarations.test.ts create mode 100644 arches_vue_utils/src/arches_vue_utils/types.ts create mode 100644 arches_vue_utils/src/arches_vue_utils/utils.spec.ts create mode 100644 arches_vue_utils/src/arches_vue_utils/utils.ts diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cef85b7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Add utils for ranking and finding labels + +### Deprecated + +### Removed + +### Security diff --git a/arches_vue_utils/src/arches_vue_utils/constants.ts b/arches_vue_utils/src/arches_vue_utils/constants.ts new file mode 100644 index 0000000..d65fc82 --- /dev/null +++ b/arches_vue_utils/src/arches_vue_utils/constants.ts @@ -0,0 +1,3 @@ +export const PREF_LABEL = "prefLabel"; +export const ALT_LABEL = "altLabel"; +export const HIDDEN_LABEL = "hiddenLabel"; diff --git a/arches_vue_utils/src/arches_vue_utils/declarations.test.ts b/arches_vue_utils/src/arches_vue_utils/declarations.test.ts deleted file mode 100644 index d351a7d..0000000 --- a/arches_vue_utils/src/arches_vue_utils/declarations.test.ts +++ /dev/null @@ -1 +0,0 @@ -// empty test file to register coverage of `declarations.d.ts` diff --git a/arches_vue_utils/src/arches_vue_utils/types.ts b/arches_vue_utils/src/arches_vue_utils/types.ts new file mode 100644 index 0000000..22c81df --- /dev/null +++ b/arches_vue_utils/src/arches_vue_utils/types.ts @@ -0,0 +1,18 @@ +export interface Language { + code: string; + default_direction: "ltr" | "rtl"; + id: number; + isdefault: boolean; + name: string; + scope: string; +} + +export interface Label { + value: string; + languageCode: string; + valuetype: "prefLabel" | "altLabel" | "hiddenLabel"; +} + +export interface Labellable { + labels: Label[]; +} diff --git a/arches_vue_utils/src/arches_vue_utils/utils.spec.ts b/arches_vue_utils/src/arches_vue_utils/utils.spec.ts new file mode 100644 index 0000000..7ef15e0 --- /dev/null +++ b/arches_vue_utils/src/arches_vue_utils/utils.spec.ts @@ -0,0 +1,88 @@ +import { + ALT_LABEL, + HIDDEN_LABEL, + PREF_LABEL, +} from "@/arches_vue_utils/constants.ts"; +import { getItemLabel, rankLabel } from "@/arches_vue_utils/utils.ts"; + +import type { Label } from "@/arches_vue_utils/types"; + +// Test utils +const asLabel = ( + valuetype: "prefLabel" | "altLabel" | "hiddenLabel", + languageCode: string, +): Label => { + return { + value: "arbitrary", + valuetype, + languageCode, + }; +}; + +const systemLanguageCode = "en-ZA"; // arbitrary + +describe("rankLabel() util", () => { + const rank = ( + valuetype: "prefLabel" | "altLabel" | "hiddenLabel", + labelLanguageCode: string, + desiredLanguageCode: string, + ) => + rankLabel( + asLabel(valuetype, labelLanguageCode), + desiredLanguageCode, + systemLanguageCode, + ); + + // Test cases inspired from python module + it("Prefers explicit region", () => { + expect(rank(PREF_LABEL, "fr-CA", "fr-CA")).toBeGreaterThan( + rank(PREF_LABEL, "fr", "fr-CA"), + ); + }); + it("Prefers pref over alt", () => { + expect(rank(PREF_LABEL, "fr", "fr-CA")).toBeGreaterThan( + rank(ALT_LABEL, "fr", "fr-CA"), + ); + }); + it("Prefers alt over hidden", () => { + expect(rank(ALT_LABEL, "fr", "fr-CA")).toBeGreaterThan( + rank(HIDDEN_LABEL, "fr", "fr-CA"), + ); + }); + it("Prefers alt label in system language to anything else", () => { + expect(rank(ALT_LABEL, systemLanguageCode, "en")).toBeGreaterThan( + rank(PREF_LABEL, "de", "en"), + ); + }); + it("Prefers region-insensitive match in system language", () => { + expect(rank(PREF_LABEL, "en", "de")).toBeGreaterThan( + rank(PREF_LABEL, "fr", "de"), + ); + }); +}); + +describe("getItemLabel() util", () => { + it("Errors if no labels", () => { + expect(() => + getItemLabel( + { labels: [] }, + systemLanguageCode, + systemLanguageCode, + ), + ).toThrow(); + }); + it("Falls back to system language", () => { + expect( + getItemLabel( + { + labels: [ + asLabel(PREF_LABEL, "de"), + asLabel(PREF_LABEL, systemLanguageCode), + ], + }, + "fr", + systemLanguageCode, + ).languageCode, + ).toEqual(systemLanguageCode); + }); +}); diff --git a/arches_vue_utils/src/arches_vue_utils/utils.ts b/arches_vue_utils/src/arches_vue_utils/utils.ts new file mode 100644 index 0000000..21be1bb --- /dev/null +++ b/arches_vue_utils/src/arches_vue_utils/utils.ts @@ -0,0 +1,58 @@ +import { ALT_LABEL, PREF_LABEL } from "@/arches_vue_utils/constants.ts"; + +import type { Label, Labellable } from "@/arches_vue_utils/types"; + +/* Port of rank_label in arches.app.utils.i18n python module */ +export const rankLabel = ( + label: Label, + preferredLanguageCode: string, + systemLanguageCode: string, +): number => { + let rank = 1; + if (label.valuetype === PREF_LABEL) { + rank = 10; + } else if (label.valuetype === ALT_LABEL) { + rank = 4; + } + + // Some arches deployments may not have standardized capitalizations. + const labelLanguageFull = label.languageCode.toLowerCase(); + const labelLanguageNoRegion = label.languageCode + .split(/[-_]/)[0] + .toLowerCase(); + const preferredLanguageFull = preferredLanguageCode.toLowerCase(); + const preferredLanguageNoRegion = preferredLanguageCode + .split(/[-_]/)[0] + .toLowerCase(); + const systemLanguageFull = systemLanguageCode.toLowerCase(); + const systemLanguageNoRegion = systemLanguageCode + .split(/[-_]/)[0] + .toLowerCase(); + + if (labelLanguageFull === preferredLanguageFull) { + rank *= 10; + } else if (labelLanguageNoRegion === preferredLanguageNoRegion) { + rank *= 5; + } else if (labelLanguageFull === systemLanguageFull) { + rank *= 3; + } else if (labelLanguageNoRegion === systemLanguageNoRegion) { + rank *= 2; + } + + return rank; +}; + +export const getItemLabel = ( + item: Labellable, + preferredLanguageCode: string, + systemLanguageCode: string, +): Label => { + if (!item.labels.length) { + throw new Error(); + } + return item.labels.sort( + (a, b) => + rankLabel(b, preferredLanguageCode, systemLanguageCode) - + rankLabel(a, preferredLanguageCode, systemLanguageCode), + )[0]; +};