Skip to content

Commit d3cde58

Browse files
committed
fix(translations,core): Align translations API with spec
1 parent b5a1867 commit d3cde58

File tree

8 files changed

+305
-43
lines changed

8 files changed

+305
-43
lines changed

packages/core/src/config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { english, Locale, RegisteredTranslations, TranslationsConfig } from "@firebase-ui/translations";
17+
import { enUs, Locale, RegisteredLocale, TranslationsConfig } from "@firebase-ui/translations";
1818
import type { FirebaseApp } from "firebase/app";
1919
import { Auth, getAuth } from "firebase/auth";
2020
import { deepMap, DeepMapStore, map } from "nanostores";
@@ -24,7 +24,7 @@ import { FirebaseUIState } from "./state";
2424
type FirebaseUIConfigurationOptions = {
2525
app: FirebaseApp;
2626
locale?: Locale | undefined;
27-
translations?: RegisteredTranslations[] | undefined;
27+
translations?: RegisteredLocale[] | undefined;
2828
behaviors?: Partial<Behavior<keyof BehaviorHandlers>>[] | undefined;
2929
recaptchaMode?: "normal" | "invisible" | undefined;
3030
};
@@ -60,7 +60,7 @@ export function initializeUI(config: FirebaseUIConfigurationOptions, name: strin
6060
config.translations ??= [];
6161

6262
// TODO: Is this right?
63-
config.translations.push(english);
63+
config.translations.push(enUs);
6464

6565
const translations = config.translations?.reduce((acc, translation) => {
6666
return {
@@ -74,7 +74,7 @@ export function initializeUI(config: FirebaseUIConfigurationOptions, name: strin
7474
deepMap<FirebaseUIConfiguration>({
7575
app: config.app,
7676
getAuth: () => getAuth(config.app),
77-
locale: config.locale ?? english.locale,
77+
locale: config.locale ?? enUs.locale,
7878
setLocale: (locale: Locale) => {
7979
const current = $config.get()[name]!;
8080
current.setKey(`locale`, locale);

packages/core/src/errors.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,15 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {
18-
english,
19-
ERROR_CODE_MAP,
20-
ErrorCode,
21-
getTranslation,
22-
Locale,
23-
TranslationsConfig,
24-
} from "@firebase-ui/translations";
17+
import { enUs, ERROR_CODE_MAP, ErrorCode, getTranslation, Locale, TranslationsConfig } from "@firebase-ui/translations";
2518
import { FirebaseUIConfiguration } from "./config";
2619
export class FirebaseUIError extends Error {
2720
code: string;
2821

2922
constructor(error: any, translations?: TranslationsConfig, locale?: Locale) {
3023
const errorCode: ErrorCode = error?.customData?.message?.match?.(/\(([^)]+)\)/)?.at(1) || error?.code || "unknown";
3124
const translationKey = ERROR_CODE_MAP[errorCode] || "unknownError";
32-
const message = getTranslation("errors", translationKey, translations, locale ?? english.locale);
25+
const message = getTranslation("errors", translationKey, translations, locale ?? enUs.locale);
3326

3427
super(message);
3528
this.name = "FirebaseUIError";

packages/core/tsconfig.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"declarationMap": true,
88
"sourceMap": true,
99
"outDir": "./dist",
10-
"rootDir": "./src",
1110
"strict": true,
1211
"noImplicitAny": true,
1312
"strictNullChecks": true,
@@ -28,7 +27,12 @@
2827
"esModuleInterop": true,
2928
"forceConsistentCasingInFileNames": true,
3029
"skipLibCheck": true,
31-
"moduleResolution": "node"
30+
"moduleResolution": "node",
31+
"baseUrl": ".",
32+
"paths": {
33+
"~/*": ["./src/*"],
34+
"@firebase-ui/translations": ["../translations/src/index.ts"]
35+
}
3236
},
3337
"include": ["src"],
3438
"exclude": ["node_modules", "dist"]

packages/translations/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@
2424
"lint": "tsc --noEmit",
2525
"format": "prettier --write \"src/**/*.ts\"",
2626
"clean": "rimraf dist",
27-
"test": "echo \"No tests specified\" && exit 0",
28-
"test:watch": "echo \"No tests specified\" && exit 0",
27+
"test": "vitest run",
28+
"test:watch": "vitest",
2929
"publish:tags": "sh -c 'TAG=\"${npm_package_name}@${npm_package_version}\"; git tag --list \"$TAG\" | grep . || git tag \"$TAG\"; git push origin \"$TAG\"'",
3030
"release": "npm run build && pnpm pack --pack-destination ../../releases/"
3131
},
3232
"devDependencies": {
3333
"prettier": "catalog:",
3434
"rimraf": "catalog:",
35+
"vitest": "catalog:",
3536
"tsup": "catalog:",
3637
"typescript": "catalog:"
3738
}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { describe, it, expect } from "vitest";
18+
import { registerLocale, enUs, type Locale, type RegisteredLocale } from "./index";
19+
import { enUS } from "./locales/en-us";
20+
import type { Translations } from "./types";
21+
import { getTranslation, ERROR_CODE_MAP } from "./mapping";
22+
import * as types from "./types";
23+
24+
describe("index.ts", () => {
25+
describe("registerLocale", () => {
26+
it("should register a locale with valid inputs", () => {
27+
const mockTranslations: Translations = {
28+
errors: {
29+
userNotFound: "Test error message",
30+
},
31+
labels: {
32+
emailAddress: "Test email label",
33+
},
34+
};
35+
36+
const result = registerLocale("en-US", mockTranslations);
37+
38+
expect(result).toEqual({
39+
locale: "en-US",
40+
translations: mockTranslations,
41+
});
42+
});
43+
44+
it("should register a locale with different locale formats", () => {
45+
const mockTranslations: Translations = {
46+
errors: {
47+
userNotFound: "Test error message",
48+
},
49+
};
50+
51+
const locales: Locale[] = ["en-US", "fr-FR", "es-ES", "custom-locale"];
52+
53+
locales.forEach((locale) => {
54+
const result = registerLocale(locale, mockTranslations);
55+
expect(result.locale).toBe(locale);
56+
expect(result.translations).toBe(mockTranslations);
57+
});
58+
});
59+
60+
it("should handle empty translations object", () => {
61+
const emptyTranslations: Translations = {};
62+
63+
const result = registerLocale("en-US", emptyTranslations);
64+
65+
expect(result).toEqual({
66+
locale: "en-US",
67+
translations: {},
68+
});
69+
});
70+
71+
it("should handle partial translations", () => {
72+
const partialTranslations: Translations = {
73+
errors: {
74+
userNotFound: "User not found",
75+
},
76+
// messages, labels, and prompts are undefined
77+
};
78+
79+
const result = registerLocale("en-US", partialTranslations);
80+
81+
expect(result.translations).toEqual(partialTranslations);
82+
expect(result.translations.errors?.userNotFound).toBe("User not found");
83+
expect(result.translations.messages).toBeUndefined();
84+
expect(result.translations.labels).toBeUndefined();
85+
expect(result.translations.prompts).toBeUndefined();
86+
});
87+
88+
it("should preserve reference to original translations object", () => {
89+
const mockTranslations: Translations = {
90+
errors: {
91+
userNotFound: "Test error message",
92+
},
93+
};
94+
95+
const result = registerLocale("en-US", mockTranslations);
96+
97+
// The translations should be the same reference
98+
expect(result.translations).toBe(mockTranslations);
99+
});
100+
});
101+
102+
describe("enUs export", () => {
103+
it("should export enUs with correct structure", () => {
104+
expect(enUs).toBeDefined();
105+
expect(enUs.locale).toBe("en-US");
106+
expect(enUs.translations).toBeDefined();
107+
});
108+
109+
it("should use the correct enUS translations", () => {
110+
expect(enUs.translations).toBe(enUS);
111+
});
112+
113+
it("should have all required translation categories", () => {
114+
expect(enUs.translations.errors).toBeDefined();
115+
expect(enUs.translations.messages).toBeDefined();
116+
expect(enUs.translations.labels).toBeDefined();
117+
expect(enUs.translations.prompts).toBeDefined();
118+
});
119+
120+
it("should have valid error translations", () => {
121+
const errors = enUs.translations.errors;
122+
expect(errors?.userNotFound).toBe("No account found with this email address");
123+
expect(errors?.wrongPassword).toBe("Incorrect password");
124+
expect(errors?.invalidEmail).toBe("Please enter a valid email address");
125+
expect(errors?.unknownError).toBe("An unexpected error occurred");
126+
});
127+
128+
it("should have valid message translations", () => {
129+
const messages = enUs.translations.messages;
130+
expect(messages?.passwordResetEmailSent).toBe("Password reset email sent successfully");
131+
expect(messages?.signInLinkSent).toBe("Sign-in link sent successfully");
132+
expect(messages?.dividerOr).toBe("or");
133+
});
134+
135+
it("should have valid label translations", () => {
136+
const labels = enUs.translations.labels;
137+
expect(labels?.emailAddress).toBe("Email Address");
138+
expect(labels?.password).toBe("Password");
139+
expect(labels?.signIn).toBe("Sign In");
140+
expect(labels?.register).toBe("Register");
141+
});
142+
143+
it("should have valid prompt translations", () => {
144+
const prompts = enUs.translations.prompts;
145+
expect(prompts?.noAccount).toBe("Don't have an account?");
146+
expect(prompts?.haveAccount).toBe("Already have an account?");
147+
expect(prompts?.signInToAccount).toBe("Sign in to your account");
148+
});
149+
});
150+
151+
describe("type exports", () => {
152+
it("should export Locale type", () => {
153+
const validLocales: Locale[] = ["en-US", "fr-FR", "es-ES", "custom-locale"];
154+
155+
validLocales.forEach((locale) => {
156+
expect(typeof locale).toBe("string");
157+
});
158+
});
159+
160+
it("should export RegisteredLocale type", () => {
161+
const mockTranslations: Translations = {
162+
errors: {
163+
userNotFound: "Test error",
164+
},
165+
};
166+
167+
const registeredLocale: RegisteredLocale = registerLocale("en-US", mockTranslations);
168+
169+
expect(registeredLocale.locale).toBe("en-US");
170+
expect(registeredLocale.translations).toBe(mockTranslations);
171+
});
172+
173+
it("should have correct type structure for RegisteredLocale", () => {
174+
const mockTranslations: Translations = {
175+
errors: {
176+
userNotFound: "Test error",
177+
},
178+
labels: {
179+
emailAddress: "Test label",
180+
},
181+
};
182+
183+
const registeredLocale: RegisteredLocale = registerLocale("test-locale", mockTranslations);
184+
185+
expect(registeredLocale).toHaveProperty("locale");
186+
expect(registeredLocale).toHaveProperty("translations");
187+
expect(typeof registeredLocale.locale).toBe("string");
188+
expect(typeof registeredLocale.translations).toBe("object");
189+
});
190+
});
191+
192+
describe("mapping exports", () => {
193+
it("should re-export mapping functions and types", () => {
194+
expect(getTranslation).toBeDefined();
195+
expect(typeof getTranslation).toBe("function");
196+
expect(ERROR_CODE_MAP).toBeDefined();
197+
expect(typeof ERROR_CODE_MAP).toBe("object");
198+
});
199+
200+
it("should have ERROR_CODE_MAP with correct structure", () => {
201+
expect(ERROR_CODE_MAP["auth/user-not-found"]).toBe("userNotFound");
202+
expect(ERROR_CODE_MAP["auth/wrong-password"]).toBe("wrongPassword");
203+
expect(ERROR_CODE_MAP["auth/invalid-email"]).toBe("invalidEmail");
204+
expect(ERROR_CODE_MAP["auth/network-request-failed"]).toBe("networkRequestFailed");
205+
});
206+
});
207+
208+
describe("type re-exports", () => {
209+
it("should re-export all types from types module", () => {
210+
const testTranslations: types.Translations = {
211+
errors: {
212+
userNotFound: "Test error",
213+
},
214+
};
215+
216+
expect(testTranslations.errors?.userNotFound).toBe("Test error");
217+
218+
// Test that we can use other types (these are compile-time checks)
219+
const testCategory: types.TranslationCategory = "errors";
220+
const testKey: types.ErrorKey = "userNotFound";
221+
222+
expect(testCategory).toBe("errors");
223+
expect(testKey).toBe("userNotFound");
224+
});
225+
});
226+
227+
describe("integration tests", () => {
228+
it("should work with custom locale registration and usage", () => {
229+
const customTranslations: Translations = {
230+
errors: {
231+
userNotFound: "Utilisateur non trouvé",
232+
wrongPassword: "Mot de passe incorrect",
233+
},
234+
labels: {
235+
emailAddress: "Adresse e-mail",
236+
password: "Mot de passe",
237+
},
238+
};
239+
240+
const customLocale = registerLocale("fr-FR", customTranslations);
241+
242+
expect(customLocale.locale).toBe("fr-FR");
243+
expect(customLocale.translations.errors?.userNotFound).toBe("Utilisateur non trouvé");
244+
expect(customLocale.translations.labels?.emailAddress).toBe("Adresse e-mail");
245+
});
246+
247+
it("should maintain type safety across all exports", () => {
248+
const mockTranslations: Translations = {
249+
errors: {
250+
userNotFound: "Test error",
251+
},
252+
};
253+
254+
const registered: RegisteredLocale = registerLocale("en-US", mockTranslations);
255+
const locale: Locale = registered.locale;
256+
257+
expect(typeof locale).toBe("string");
258+
expect(registered.translations).toBe(mockTranslations);
259+
});
260+
});
261+
});

packages/translations/src/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ import { Translations } from "./types";
2020
export type * from "./types";
2121
export * from "./mapping";
2222

23-
export type Locale = "en-US" | `${string}-${string}`;
23+
export type Locale = "en-US" | `${string}-${string}` | string;
2424

25-
export function customLanguage(locale: Locale, translations: Translations) {
25+
export function registerLocale(locale: Locale, translations: Translations) {
2626
return {
2727
locale,
2828
translations,
2929
};
3030
}
3131

32-
export const english = customLanguage("en-US", enUS);
32+
export const enUs = registerLocale("en-US", enUS);
3333

34-
export type RegisteredTranslations = ReturnType<typeof customLanguage>;
34+
export type RegisteredLocale = ReturnType<typeof registerLocale>;

0 commit comments

Comments
 (0)