Skip to content

Commit 510e63c

Browse files
authored
Merge pull request #2142 from Parvinmh/languageCode
language code string for built-in languages
2 parents 35fe46d + bf0241c commit 510e63c

File tree

8 files changed

+451
-59
lines changed

8 files changed

+451
-59
lines changed

example/html-tooltip/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ <h4>Section Six</h4>
7474
var intro = introJs.tour();
7575
intro.setOptions({
7676
hideNext: true,
77+
language: 'de_DE',
7778
steps: [
7879
{
7980
title: '<p>Welcome</p>',

src/i18n/language.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Translator, getAvailableLanguages, LanguageCode } from "./language";
2+
3+
// Store original navigator
4+
const originalNavigator = global.navigator;
5+
6+
// Mock navigator object
7+
const createMockNavigator = (language?: string, userLanguage?: string) => {
8+
const mockNavigator: any = {};
9+
10+
if (language !== undefined) mockNavigator.language = language;
11+
if (userLanguage !== undefined) mockNavigator.userLanguage = userLanguage;
12+
13+
if (language === undefined && userLanguage === undefined) {
14+
mockNavigator.language = "en-US";
15+
}
16+
17+
Object.defineProperty(global, "navigator", {
18+
value: mockNavigator,
19+
writable: true,
20+
configurable: true,
21+
});
22+
23+
return mockNavigator;
24+
};
25+
26+
describe("Translator", () => {
27+
beforeEach(() => createMockNavigator());
28+
afterAll(() => {
29+
Object.defineProperty(global, "navigator", {
30+
value: originalNavigator,
31+
writable: true,
32+
configurable: true,
33+
});
34+
});
35+
36+
describe("Constructor", () => {
37+
it("should use provided language code", () => {
38+
const translator = new Translator("es_ES");
39+
expect(translator.translate("buttons.next")).toBe("Siguiente");
40+
});
41+
42+
it("should detect browser language and match locale", () => {
43+
createMockNavigator("fr-FR");
44+
const translator = new Translator();
45+
expect(translator.translate("buttons.next")).toBe("Suivant");
46+
});
47+
48+
it("should fallback to en_US for unsupported languages", () => {
49+
createMockNavigator("zh-CN");
50+
const translator = new Translator();
51+
expect(translator.translate("buttons.next")).toBe("Next");
52+
});
53+
54+
it("should handle underscores and dashes in language codes", () => {
55+
createMockNavigator("es_ES");
56+
const translator1 = new Translator();
57+
expect(translator1.translate("buttons.next")).toBe("Siguiente");
58+
59+
createMockNavigator("fr-FR");
60+
const translator2 = new Translator();
61+
expect(translator2.translate("buttons.next")).toBe("Suivant");
62+
});
63+
});
64+
65+
describe("setLanguage", () => {
66+
it("should change active language correctly", () => {
67+
const translator = new Translator("en_US");
68+
translator.setLanguage("fr_FR");
69+
expect(translator.translate("buttons.done")).toBe("Terminé");
70+
71+
translator.setLanguage("fa_IR");
72+
expect(translator.translate("buttons.done")).toBe("پایان");
73+
});
74+
75+
it("should work with all supported languages", () => {
76+
const translator = new Translator();
77+
(["en_US", "es_ES", "fr_FR", "de_DE", "fa_IR"] as LanguageCode[]).forEach(
78+
(code) => {
79+
translator.setLanguage(code);
80+
expect(typeof translator.translate("buttons.next")).toBe("string");
81+
}
82+
);
83+
});
84+
});
85+
86+
describe("translate", () => {
87+
let translator: Translator;
88+
89+
beforeEach(() => (translator = new Translator("en_US")));
90+
91+
it("should translate standard keys", () => {
92+
expect(translator.translate("buttons.next")).toBe("Next");
93+
expect(translator.translate("buttons.prev")).toBe("Back");
94+
expect(translator.translate("buttons.done")).toBe("Done");
95+
});
96+
97+
it("should return key if not found", () => {
98+
expect(translator.translate("nonexistent.key")).toBe("nonexistent.key");
99+
});
100+
});
101+
102+
describe("getAvailableLanguages", () => {
103+
it("should return all language codes", () => {
104+
const codes = getAvailableLanguages();
105+
expect(codes).toEqual(
106+
expect.arrayContaining(["en_US", "es_ES", "fr_FR", "de_DE", "fa_IR"])
107+
);
108+
});
109+
});
110+
111+
describe("Integration with Tour options", () => {
112+
class MockTour {
113+
private _options: any = {};
114+
setOptions(options: any) {
115+
const processed = { ...options };
116+
if (processed.language) {
117+
const translator = new Translator(processed.language);
118+
processed.nextLabel =
119+
processed.nextLabel ?? translator.translate("buttons.next");
120+
processed.prevLabel =
121+
processed.prevLabel ?? translator.translate("buttons.prev");
122+
processed.doneLabel =
123+
processed.doneLabel ?? translator.translate("buttons.done");
124+
}
125+
this._options = { ...this._options, ...processed };
126+
}
127+
getOption(key: string) {
128+
return this._options[key];
129+
}
130+
}
131+
132+
it("should translate labels based on language code", () => {
133+
const tour = new MockTour();
134+
tour.setOptions({ language: "es_ES" });
135+
expect(tour.getOption("nextLabel")).toBe("Siguiente");
136+
expect(tour.getOption("prevLabel")).toBe("Atrás");
137+
expect(tour.getOption("doneLabel")).toBe("Hecho");
138+
});
139+
140+
it("should not override custom labels", () => {
141+
const tour = new MockTour();
142+
tour.setOptions({ language: "fr_FR", nextLabel: "Custom Next" });
143+
expect(tour.getOption("nextLabel")).toBe("Custom Next");
144+
expect(tour.getOption("doneLabel")).toBe("Terminé");
145+
});
146+
147+
it("should handle multiple languages sequentially", () => {
148+
const tour = new MockTour();
149+
tour.setOptions({ language: "de_DE" });
150+
expect(tour.getOption("nextLabel")).toBe("Weiter");
151+
152+
tour.setOptions({ language: "fa_IR" });
153+
expect(tour.getOption("nextLabel")).toBe("بعدی");
154+
});
155+
});
156+
});

src/i18n/language.ts

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,81 @@
11
import enUS from "./en_US";
22
import faIR from "./fa_IR";
3-
import de_DE from "./de_DE";
3+
import deDE from "./de_DE";
44
import esES from "./es_ES";
55
import frFR from "./fr_FR";
66

77
type MessageFormat = (...args: any[]) => string;
8-
type Message = string | MessageFormat;
9-
export type Language = { [key: string]: Message | Language };
108

11-
const languages: Record<string, Language> = {
9+
const languages = {
1210
en_US: enUS,
1311
fa_IR: faIR,
14-
de_DE: de_DE,
12+
de_DE: deDE,
1513
es_ES: esES,
1614
fr_FR: frFR,
17-
};
15+
} as const;
16+
17+
export type LanguageCode = keyof typeof languages;
18+
export const DefaultLanguage: LanguageCode = "en_US";
19+
20+
/**
21+
* Get all available language codes
22+
*/
23+
export function getAvailableLanguages(): LanguageCode[] {
24+
return Object.keys(languages) as LanguageCode[];
25+
}
1826

1927
export class Translator {
20-
private _language: Language;
28+
private _languageCode: LanguageCode;
2129

22-
constructor(language?: Language) {
23-
if (language) {
24-
this._language = language;
30+
constructor(languageCode?: LanguageCode) {
31+
if (languageCode && languages[languageCode]) {
32+
this._languageCode = languageCode;
2533
} else {
2634
const rawLang = (
2735
navigator.language ||
2836
(navigator as any).userLanguage ||
29-
"en-US"
37+
DefaultLanguage
3038
).replace("-", "_");
3139

32-
const normalizedLang = Object.keys(languages).find(
40+
const normalizedLang = (Object.keys(languages) as LanguageCode[]).find(
3341
(key) => key.toLowerCase() === rawLang.toLowerCase()
3442
);
3543

36-
this._language = normalizedLang ? languages[normalizedLang] : enUS;
44+
this._languageCode = normalizedLang ?? DefaultLanguage;
3745
}
3846
}
3947

40-
setLanguage(language: Language) {
41-
this._language = language;
48+
setLanguage(code: LanguageCode) {
49+
if (languages[code]) {
50+
this._languageCode = code;
51+
}
52+
}
53+
54+
getLanguage(): LanguageCode {
55+
return this._languageCode;
56+
}
57+
58+
private get messages() {
59+
return languages[this._languageCode];
4260
}
4361

4462
private getString(
4563
message: string,
46-
lang: Language = this._language
64+
langObj: any = this.messages
4765
): MessageFormat | null {
48-
if (!lang || !message) return null;
66+
if (!langObj || !message) return null;
4967

5068
const splitted = message.split(".");
5169
const key = splitted[0];
5270

53-
if (lang[key]) {
54-
const val = lang[key];
55-
71+
if (langObj[key]) {
72+
const val = langObj[key];
5673
if (typeof val === "string") {
5774
return (): string => val;
5875
} else if (typeof val === "function") {
5976
return val;
6077
} else {
61-
return this.getString(splitted.slice(1).join("."), val as Language);
78+
return this.getString(splitted.slice(1).join("."), val);
6279
}
6380
}
6481

0 commit comments

Comments
 (0)