Skip to content

Commit

Permalink
Merge pull request #31 from sillsdev/fuzzy_stories
Browse files Browse the repository at this point in the history
fix: preload search string, more demos (#31)
  • Loading branch information
andrew-polk authored Nov 7, 2024
2 parents 783d5c1 + d0a387f commit cc12a72
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 168 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
export * from "./useLanguageChooser";
export {
isUnlistedLanguage,
createTagFromOrthography,
// We don't want to export parseLangtagForLangChooser because it is not a comprehensive langtag parser.
// Just built to handle the langtags output by the language chooser and the libPalasso language picker that was in BloomDesktop.
} from "./languageTagHandling";
export * from "./languageTagHandling";
export type {
IOrthography,
ICustomizableLanguageDetails,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,99 +1,101 @@
import { expect, it, describe } from "vitest";
import { parseLangtagForLangChooser } from "./languageTagHandling";
import { parseLangtagFromLangChooser } from "./languageTagHandling";
import { getRegionBySubtag } from "@ethnolib/find-language";
describe("Tag parsing", () => {
it("should find a language by 2 letter language subtag", () => {
expect(parseLangtagForLangChooser("ja")?.language?.exonym).toEqual(
expect(parseLangtagFromLangChooser("ja")?.language?.exonym).toEqual(
"Japanese"
);
});
it("should return undefined if no language found", () => {
expect(parseLangtagForLangChooser("")).toBeUndefined();
expect(parseLangtagForLangChooser("thisistoolong")).toBeUndefined();
expect(parseLangtagForLangChooser("xxx")).toBeUndefined();
expect(parseLangtagFromLangChooser("")).toBeUndefined();
expect(parseLangtagFromLangChooser("thisistoolong")).toBeUndefined();
expect(parseLangtagFromLangChooser("xxx")).toBeUndefined();
});
it("should find a region by its tag", () => {
expect(
parseLangtagForLangChooser("sqa-Latn-NG")?.customDetails?.region?.name
parseLangtagFromLangChooser("sqa-Latn-NG")?.customDetails?.region?.name
).toEqual("Nigeria");
});
it("should not return a region if the tag does not contain an explicit and valid region code", () => {
expect(parseLangtagForLangChooser("en-Latn")).toBeTruthy();
expect(parseLangtagFromLangChooser("en-Latn")).toBeTruthy();
expect(
parseLangtagForLangChooser("en-Latn")?.customDetails?.region
parseLangtagFromLangChooser("en-Latn")?.customDetails?.region
).toBeUndefined();
// ZZ should be an invalid region code:
expect(getRegionBySubtag("ZZ")).toBeUndefined();
expect(
parseLangtagForLangChooser("en-Latn-ZZ")?.customDetails?.region
parseLangtagFromLangChooser("en-Latn-ZZ")?.customDetails?.region
).toBeUndefined();
expect(
parseLangtagForLangChooser("ssh-Arab")?.customDetails?.region
parseLangtagFromLangChooser("ssh-Arab")?.customDetails?.region
).toBeUndefined();
});
it("should find a script by its tag", () => {
expect(parseLangtagForLangChooser("uz-Sogd")?.script?.name).toEqual(
expect(parseLangtagFromLangChooser("uz-Sogd")?.script?.name).toEqual(
"Sogdian"
);
});
it("should find valid information even if script, region or dialect is not typically associated with that language", () => {
const result = parseLangtagForLangChooser("ixl-Cyrl-JP-x-foobar"); // Ixil (normally latin script, guatemala region)
const result = parseLangtagFromLangChooser("ixl-Cyrl-JP-x-foobar"); // Ixil (normally latin script, guatemala region)
expect(result?.language?.exonym).toEqual("Ixil");
expect(result?.script?.name).toEqual("Cyrillic");
expect(result?.customDetails?.region?.name).toEqual("Japan");
});

it("should find the correct implied scripts", () => {
expect(parseLangtagForLangChooser("uz")?.script?.name).toEqual("Latin");
expect(parseLangtagForLangChooser("uz-x-barfoo")?.script?.name).toEqual(
expect(parseLangtagFromLangChooser("uz")?.script?.name).toEqual("Latin");
expect(parseLangtagFromLangChooser("uz-x-barfoo")?.script?.name).toEqual(
"Latin"
);
expect(parseLangtagForLangChooser("uz-AF")?.script?.name).toEqual("Arabic");
expect(parseLangtagForLangChooser("uz-AF-x-foobar")?.script?.name).toEqual(
expect(parseLangtagFromLangChooser("uz-AF")?.script?.name).toEqual(
"Arabic"
);
expect(parseLangtagFromLangChooser("uz-AF-x-foobar")?.script?.name).toEqual(
"Arabic"
);
});

it("should put private use subtags into dialect field", () => {
expect(
parseLangtagForLangChooser("en-Latn-x-foo")?.customDetails?.dialect
parseLangtagFromLangChooser("en-Latn-x-foo")?.customDetails?.dialect
).toEqual("foo");
});
it("should be case insensitive", () => {
const result = parseLangtagForLangChooser("uZb-CyRl-aF");
const result = parseLangtagFromLangChooser("uZb-CyRl-aF");
expect(result?.language?.exonym).toEqual("Uzbek");
expect(result?.script?.name).toEqual("Cyrillic");
expect(result?.customDetails?.region?.name).toEqual("Afghanistan");
});
it("should work for all combos of present and absent subtags", () => {
// ssh, ssh-Arab, ssh-AE, ssh-x-foobar, ssh-Arab-AE, ssh-Arab-x-foobar, ssh-AE-x-foobar, ssh-Arab-AE-x-foobar
const ssh_result = parseLangtagForLangChooser("ssh");
const ssh_result = parseLangtagFromLangChooser("ssh");
expect(ssh_result?.language?.exonym).toEqual("Shihhi Arabic");
expect(ssh_result?.script?.name).toEqual("Arabic");
expect(ssh_result?.customDetails?.region?.name).toBeUndefined();
expect(ssh_result?.customDetails?.dialect).toBeUndefined();

const ssh_Arab_result = parseLangtagForLangChooser("ssh-Arab");
const ssh_Arab_result = parseLangtagFromLangChooser("ssh-Arab");
expect(ssh_Arab_result?.language?.exonym).toEqual("Shihhi Arabic");
expect(ssh_Arab_result?.script?.name).toEqual("Arabic");
expect(ssh_Arab_result?.customDetails?.region?.name).toBeUndefined();
expect(ssh_Arab_result?.customDetails?.dialect).toBeUndefined();

const ssh_AE_result = parseLangtagForLangChooser("ssh-AE");
const ssh_AE_result = parseLangtagFromLangChooser("ssh-AE");
expect(ssh_AE_result?.language?.exonym).toEqual("Shihhi Arabic");
expect(ssh_AE_result?.script?.name).toEqual("Arabic");
expect(ssh_AE_result?.customDetails?.region?.name).toEqual(
"United Arab Emirates"
);
expect(ssh_AE_result?.customDetails?.dialect).toBeUndefined();

const ssh_x_foobar_result = parseLangtagForLangChooser("ssh-x-foobar");
const ssh_x_foobar_result = parseLangtagFromLangChooser("ssh-x-foobar");
expect(ssh_x_foobar_result?.language?.exonym).toEqual("Shihhi Arabic");
expect(ssh_x_foobar_result?.script?.name).toEqual("Arabic");
expect(ssh_x_foobar_result?.customDetails?.region?.name).toBeUndefined();
expect(ssh_x_foobar_result?.customDetails?.dialect).toEqual("foobar");

const ssh_Arab_AE_result = parseLangtagForLangChooser("ssh-Arab-AE");
const ssh_Arab_AE_result = parseLangtagFromLangChooser("ssh-Arab-AE");
expect(ssh_Arab_AE_result?.language?.exonym).toEqual("Shihhi Arabic");
expect(ssh_Arab_AE_result?.script?.name).toEqual("Arabic");
expect(ssh_Arab_AE_result?.customDetails?.region?.name).toEqual(
Expand All @@ -102,7 +104,7 @@ describe("Tag parsing", () => {
expect(ssh_Arab_AE_result?.customDetails?.dialect).toBeUndefined();

const ssh_Arab_x_foobar_result =
parseLangtagForLangChooser("ssh-Arab-x-foobar");
parseLangtagFromLangChooser("ssh-Arab-x-foobar");
expect(ssh_Arab_x_foobar_result?.language?.exonym).toEqual("Shihhi Arabic");
expect(ssh_Arab_x_foobar_result?.script?.name).toEqual("Arabic");
expect(
Expand All @@ -111,15 +113,15 @@ describe("Tag parsing", () => {
expect(ssh_Arab_x_foobar_result?.customDetails?.dialect).toEqual("foobar");
});

const ssh_AE_x_foobar_result = parseLangtagForLangChooser("ssh-AE-x-foobar");
const ssh_AE_x_foobar_result = parseLangtagFromLangChooser("ssh-AE-x-foobar");
expect(ssh_AE_x_foobar_result?.language?.exonym).toEqual("Shihhi Arabic");
expect(ssh_AE_x_foobar_result?.script?.name).toEqual("Arabic");
expect(ssh_AE_x_foobar_result?.customDetails?.region?.name).toEqual(
"United Arab Emirates"
);
expect(ssh_AE_x_foobar_result?.customDetails?.dialect).toEqual("foobar");

const ssh_Arab_AE_x_foobar_result = parseLangtagForLangChooser(
const ssh_Arab_AE_x_foobar_result = parseLangtagFromLangChooser(
"ssh-Arab-AE-x-foobar"
);
expect(ssh_Arab_AE_x_foobar_result?.language?.exonym).toEqual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,14 @@ export function createTagFromOrthography(orthography: IOrthography) {
});
}

// We don't want to export this outside of the language-chooser-react-hook package
// because it is not a comprehensive langtag parser. It's just built to handle the
// This is not a comprehensive language tag parser. It's just built to handle the
// langtags output by the language chooser and the libPalasso language picker that
// was in BloomDesktop. The languageTag must be the default language subtag for
// that language (the first part of the "tag" field of langtags.json), which may
// be a 2-letter code even if an equivalent ISO 639-3 code exists. We also may not
// correctly handle other BCP-47 langtag corner cases, e.g. irregular codes,
// be a 2-letter code even if an equivalent ISO 639-3 code exists. This parser is also not
// designed to handle other BCP-47 langtag corner cases, e.g. irregular codes,
// extension codes, langtags with both macrolanguage code and language code
export function parseLangtagForLangChooser(
export function parseLangtagFromLangChooser(
languageTag: string // must be the default language subtag for the language
): IOrthography | undefined {
const parts = languageTag.split(/-[xX]-/);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { useMemo, useState } from "react";
import { FuseResult } from "fuse.js";
import {
ICustomizableLanguageDetails,
IOrthography,
isUnlistedLanguage,
parseLangtagForLangChooser,
parseLangtagFromLangChooser,
UNLISTED_LANGUAGE,
} from "./languageTagHandling";

Expand All @@ -31,7 +32,8 @@ export interface ILanguageChooser {
) => void;
selectUnlistedLanguage: () => void;
resetTo: (
initialLanguageTag: string,
searchString: string,
selectionLanguageTag?: string,
initialCustomDisplayName?: string
) => void;
}
Expand Down Expand Up @@ -80,28 +82,32 @@ export const useLanguageChooser = (
// For reopening to a specific selection. We should then also set the search string
// such that the selected language is visible.
function resetTo(
initialLanguageTag: string,
searchString: string,
selectionLanguageTag?: string, // if present, the language in selectionLanguageTag must be a result of this search string or selection won't display
initialCustomDisplayName?: string // all info can be captured in language tag except display name
) {
// clear everything
setSelectedLanguage(undefined);
setSelectedScript(undefined);
clearCustomizableLanguageDetails();

const initialSelections = parseLangtagForLangChooser(initialLanguageTag);
if (initialSelections) {
// TODO if there is a language code that is also the start of so many language names
// that the language card with that code isn't initially visible and one must scroll to see it,
// scroll to it
onSearchStringChange(initialSelections.language?.languageSubtag || "");
setSelectedLanguage(initialSelections.language);
saveLanguageDetails(
{
...initialSelections.customDetails,
displayName: initialCustomDisplayName,
} as ICustomizableLanguageDetails,
initialSelections?.script
);
onSearchStringChange(searchString);

const initialSelections = parseLangtagFromLangChooser(
selectionLanguageTag || ""
);

if (initialSelections?.language) {
// TODO future work: if the selection language is lower in the search results such that its
// language card isn't initially visible, we should automatically scroll to it
toggleSelectLanguage(initialSelections.language);
if (initialSelections?.script) {
toggleSelectScript(initialSelections.script);
}
setCustomizableLanguageDetails((c) => {
// toggleSelectLanguage will have set a default display name. We want to use it unless
// it is overridden by a initialCustomDisplayName
return {
...(initialSelections?.customDetails ||
({} as ICustomizableLanguageDetails)),
displayName: initialCustomDisplayName || c.displayName,
};
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export const LanguageChooser: React.FunctionComponent<{
results: FuseResult<ILanguage>[],
searchString: string
) => ILanguage[];
initialLanguageTag?: string;
initialSearchString?: string;
initialSelectionLanguageTag?: string;
initialCustomDisplayName?: string;
onClose: (
languageSelection: IOrthography | undefined,
Expand All @@ -60,9 +61,14 @@ export const LanguageChooser: React.FunctionComponent<{
const lp: ILanguageChooser = useLanguageChooser(props.searchResultModifier);

useEffect(() => {
lp.resetTo(props.initialLanguageTag || "", props.initialCustomDisplayName);
// We only want this to run once
// eslint-disable-next-line react-hooks/exhaustive-deps
if (searchInputRef) {
searchInputRef.value = props.initialSearchString || "";
}
lp.resetTo(
props.initialSearchString || "",
props.initialSelectionLanguageTag,
props.initialCustomDisplayName
);
}, []);

const [customizeLanguageDialogOpen, setCustomizeLanguageDialogOpen] =
Expand Down Expand Up @@ -195,7 +201,7 @@ export const LanguageChooser: React.FunctionComponent<{
/>
<OutlinedInput
type="text"
inputRef={(el) => (searchInputRef = el)}
inputRef={(el) => (searchInputRef = el)} // for displaying initial search string
css={css`
background-color: white;
margin-bottom: 10px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,49 @@ export default meta;
type Story = StoryObj<typeof DialogDemo>;

export const Primary: Story = {
args: {},
};

export const ReopenWithLanguageInformation: Story = {
args: {
alreadyFilled: false,
initialSearchString: "Uz",
initialLanguageTag: "uz-Cyrl",
initialCustomDisplayName: "ÖzbekCustomizedName",
},
};

export const ReopenWithLanguageInformation: Story = {
export const SearchWithTypo: Story = {
args: {
initialSearchString: "jpanese",
},
};

export const SearchByRegionName: Story = {
args: {
initialSearchString: "afghanistan",
},
};

export const SearchByAlternativeName: Story = {
args: {
initialSearchString: "barbadian creole english",
},
};

export const SearchByISO639Code: Story = {
args: {
alreadyFilled: true,
initialSearchString: "sdk",
},
};

export const AdditionalRightPanelComponent: Story = {
args: {
alreadyFilled: false,
demoRightPanelComponent: true,
},
};

export const InASmallDialog: Story = {
args: {
alreadyFilled: false,
dialogHeight: "350px",
dialogWidth: "650px",
},
Expand Down
Loading

0 comments on commit cc12a72

Please sign in to comment.