From 2cfd52415c0c53b70b039c241a1bb00144216fef Mon Sep 17 00:00:00 2001 From: simojenki Date: Mon, 16 Aug 2021 10:50:56 +1000 Subject: [PATCH] Case-insensitive lang search for i8n, along with support for match just lang, without region, ie. 'en' == 'en-US' --- package.json | 2 +- src/i8n.ts | 11 +++- src/server.ts | 5 +- src/smapi.ts | 5 +- tests/i8n.test.ts | 159 ++++++++++++++++++++++++++++------------------ 5 files changed, 114 insertions(+), 68 deletions(-) diff --git a/package.json b/package.json index 1da9a17..8ad78bf 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "scripts": { "clean": "rm -Rf build", "build": "tsc", - "dev": "BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=true BONOB_SONOS_AUTO_REGISTER=false nodemon ./src/app.ts", + "dev": "BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=true nodemon ./src/app.ts", "register-dev": "ts-node ./src/register.ts http://$(hostname):4534", "test": "jest --testPathIgnorePatterns=build" } diff --git a/src/i8n.ts b/src/i8n.ts index 538894a..f7f613f 100644 --- a/src/i8n.ts +++ b/src/i8n.ts @@ -106,6 +106,13 @@ const translations: Record> = { }, }; +const translationsLookup = Object.keys(translations).reduce((lookups, lang) => { + lookups.set(lang, translations[lang as LANG]); + lookups.set(lang.toLocaleLowerCase(), translations[lang as LANG]); + lookups.set(lang.toLocaleLowerCase().split("-")[0]!, translations[lang as LANG]); + return lookups; +}, new Map>()) + export const randomLang = () => _.shuffle(["en-US", "nl-NL"])[0]!; export const asLANGs = (acceptLanguageHeader: string | undefined) => @@ -134,8 +141,8 @@ export const keys = (lang: LANG = "en-US") => Object.keys(translations[lang]); export default (serviceName: string): I8N => (...langs: string[]): Lang => { - const langToUse = - langs.map((l) => translations[l as LANG]).find((it) => it) || + const langToUse = + langs.map((l) => translationsLookup.get(l as LANG)).find((it) => it) || translations["en-US"]; return (key: KEY) => { const value = langToUse[key]?.replace( diff --git a/src/server.ts b/src/server.ts index fd046f8..02b2932 100644 --- a/src/server.ts +++ b/src/server.ts @@ -87,7 +87,10 @@ function server( app.set("view engine", "eta"); app.set("views", "./web/views"); - const langFor = (req: Request) => i8n(...asLANGs(req.headers["accept-language"])) + const langFor = (req: Request) => { + logger.debug(`${req.path} (req[accept-language]=${req.headers["accept-language"]})`); + return i8n(...asLANGs(req.headers["accept-language"])); + } app.get("/", (req, res) => { const lang = langFor(req); diff --git a/src/smapi.ts b/src/smapi.ts index e39399e..70bb450 100644 --- a/src/smapi.ts +++ b/src/smapi.ts @@ -536,9 +536,10 @@ function bindSmapiSoapServiceToExpress( .then(splitId(id)) .then(({ musicLibrary, accessToken, type, typeId }) => { const paging = { _index: index, _count: count }; - const lang = i8n((headers["accept-language"] || "en-US") as LANG); + const acceptLanguage = headers["accept-language"]; + const lang = i8n((acceptLanguage || "en-US") as LANG); logger.debug( - `Fetching metadata type=${type}, typeId=${typeId}, lang=${lang}` + `Fetching metadata type=${type}, typeId=${typeId}, acceptLanguage=${acceptLanguage}` ); const albums = (q: AlbumQuery): Promise => diff --git a/tests/i8n.test.ts b/tests/i8n.test.ts index d804a6d..f939dfc 100644 --- a/tests/i8n.test.ts +++ b/tests/i8n.test.ts @@ -54,79 +54,129 @@ describe("i8n", () => { describe("fetching translations", () => { describe("with a single lang", () => { - describe("and there is no templating", () => { - it("should return the value", () => { - expect(i8n("foo")("en-US")("artists")).toEqual("Artists"); - expect(i8n("foo")("nl-NL")("artists")).toEqual("Artiesten"); + describe("and the lang is not represented", () => { + describe("and there is no templating", () => { + it("should return the en-US value", () => { + expect(i8n("foo")("en-AU" as LANG)("artists")).toEqual("Artists"); + }); + }); + + describe("and there is templating of the service name", () => { + it("should return the en-US value templated", () => { + expect(i8n("service123")("en-AU" as LANG)("AppLinkMessage")).toEqual( + "Linking sonos with service123" + ); + }); }); }); - - describe("and there is templating of the service name", () => { - it("should return the value", () => { - expect(i8n("service123")("en-US")("AppLinkMessage")).toEqual( - "Linking sonos with service123" - ); - expect(i8n("service456")("nl-NL")("AppLinkMessage")).toEqual( - "Sonos koppelen aan service456" - ); + + describe("and the lang is represented", () => { + describe("and there is no templating", () => { + it("should return the value", () => { + expect(i8n("foo")("en-US")("artists")).toEqual("Artists"); + expect(i8n("foo")("nl-NL")("artists")).toEqual("Artiesten"); + }); + }); + + describe("and there is templating of the service name", () => { + it("should return the value", () => { + expect(i8n("service123")("en-US")("AppLinkMessage")).toEqual( + "Linking sonos with service123" + ); + expect(i8n("service456")("nl-NL")("AppLinkMessage")).toEqual( + "Sonos koppelen aan service456" + ); + }); }); }); }); describe("with multiple langs", () => { - describe("and the first lang is a match", () => { + function itShouldReturn(serviceName: string, langs: string[], key: KEY, expected: string) { + it(`should return '${expected}' for the serviceName=${serviceName}, langs=${langs}`, () => { + expect(i8n(serviceName)(...langs)(key)).toEqual(expected); + }); + }; + + describe("and the first lang is an exact match", () => { describe("and there is no templating", () => { - it("should return the value for the first lang", () => { - expect(i8n("foo")("en-US", "nl-NL")("artists")).toEqual("Artists"); - expect(i8n("foo")("nl-NL", "en-US")("artists")).toEqual("Artiesten"); - }); + itShouldReturn("foo", ["en-US", "nl-NL"], "artists", "Artists"); + itShouldReturn("foo", ["nl-NL", "en-US"], "artists", "Artiesten"); }); describe("and there is templating of the service name", () => { - it("should return the value for the firt lang", () => { - expect(i8n("service123")("en-US", "nl-NL")("AppLinkMessage")).toEqual( - "Linking sonos with service123" - ); - expect(i8n("service456")("nl-NL", "en-US")("AppLinkMessage")).toEqual( - "Sonos koppelen aan service456" - ); - }); + itShouldReturn("service123", ["en-US", "nl-NL"], "AppLinkMessage", "Linking sonos with service123"); + itShouldReturn("service456", ["nl-NL", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456"); }); }); - describe("and the first lang is not a match, however there is a match in the provided langs", () => { + describe("and the first lang is a case insensitive match", () => { describe("and there is no templating", () => { - it("should return the value for the first lang", () => { - expect(i8n("foo")("something", "en-US", "nl-NL")("artists")).toEqual("Artists"); - expect(i8n("foo")("something", "nl-NL", "en-US")("artists")).toEqual("Artiesten"); - }); + itShouldReturn("foo", ["en-us", "nl-NL"], "artists", "Artists"); + itShouldReturn("foo", ["nl-nl", "en-US"], "artists", "Artiesten"); }); describe("and there is templating of the service name", () => { - it("should return the value for the firt lang", () => { - expect(i8n("service123")("something", "en-US", "nl-NL")("AppLinkMessage")).toEqual( - "Linking sonos with service123" - ); - expect(i8n("service456")("something", "nl-NL", "en-US")("AppLinkMessage")).toEqual( - "Sonos koppelen aan service456" - ); - }); + itShouldReturn("service123", ["en-us", "nl-NL"], "AppLinkMessage", "Linking sonos with service123"); + itShouldReturn("service456", ["nl-nl", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456"); + }); + }); + + describe("and the first lang is a lang match without region", () => { + describe("and there is no templating", () => { + itShouldReturn("foo", ["en", "nl-NL"], "artists", "Artists"); + itShouldReturn("foo", ["nl", "en-US"], "artists", "Artiesten"); + }); + + describe("and there is templating of the service name", () => { + itShouldReturn("service123", ["en", "nl-NL"], "AppLinkMessage", "Linking sonos with service123"); + itShouldReturn("service456", ["nl", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456"); + }); + }); + + describe("and the first lang is not a match, however there is an exact match in the provided langs", () => { + describe("and there is no templating", () => { + itShouldReturn("foo", ["something", "en-US", "nl-NL"], "artists", "Artists") + itShouldReturn("foo", ["something", "nl-NL", "en-US"], "artists", "Artiesten") + }); + + describe("and there is templating of the service name", () => { + itShouldReturn("service123", ["something", "en-US", "nl-NL"], "AppLinkMessage", "Linking sonos with service123") + itShouldReturn("service456", ["something", "nl-NL", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456") + }); + }); + + describe("and the first lang is not a match, however there is a case insensitive match in the provided langs", () => { + describe("and there is no templating", () => { + itShouldReturn("foo", ["something", "en-us", "nl-nl"], "artists", "Artists") + itShouldReturn("foo", ["something", "nl-nl", "en-us"], "artists", "Artiesten") + }); + + describe("and there is templating of the service name", () => { + itShouldReturn("service123", ["something", "en-us", "nl-nl"], "AppLinkMessage", "Linking sonos with service123") + itShouldReturn("service456", ["something", "nl-nl", "en-us"], "AppLinkMessage", "Sonos koppelen aan service456") + }); + }); + + describe("and the first lang is not a match, however there is a lang match without region", () => { + describe("and there is no templating", () => { + itShouldReturn("foo", ["something", "en", "nl-nl"], "artists", "Artists") + itShouldReturn("foo", ["something", "nl", "en-us"], "artists", "Artiesten") + }); + + describe("and there is templating of the service name", () => { + itShouldReturn("service123", ["something", "en", "nl-nl"], "AppLinkMessage", "Linking sonos with service123") + itShouldReturn("service456", ["something", "nl", "en-us"], "AppLinkMessage", "Sonos koppelen aan service456") }); }); describe("and no lang is a match", () => { describe("and there is no templating", () => { - it("should return the value for the first lang", () => { - expect(i8n("foo")("something", "something2")("artists")).toEqual("Artists"); - }); + itShouldReturn("foo", ["something", "something2"], "artists", "Artists") }); describe("and there is templating of the service name", () => { - it("should return the value for the firt lang", () => { - expect(i8n("service123")("something", "something2")("AppLinkMessage")).toEqual( - "Linking sonos with service123" - ); - }); + itShouldReturn("service123", ["something", "something2"], "AppLinkMessage", "Linking sonos with service123") }); }); }); @@ -139,20 +189,5 @@ describe("i8n", () => { }); }); - describe("when the lang is not represented", () => { - describe("and there is no templating", () => { - it("should return the en-US value", () => { - expect(i8n("foo")("en-AU" as LANG)("artists")).toEqual("Artists"); - }); - }); - - describe("and there is templating of the service name", () => { - it("should return the en-US value templated", () => { - expect(i8n("service123")("en-AU" as LANG)("AppLinkMessage")).toEqual( - "Linking sonos with service123" - ); - }); - }); - }); }); });