-
Notifications
You must be signed in to change notification settings - Fork 710
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: deprecate
i18n-js
package with custom i18n
class translati…
…on (#1736) * chore: deprecate i18n-js library and introduce custom made translation class * chore: tweak comments * chore: changeset * chore: tweak test name * chore: remove console.log * chore: tweak test names * chore: bump I18n into locale dir * chore: tweak changeset --------- Co-authored-by: Daniel Sinclair <d@niel.nyc>
- Loading branch information
1 parent
c0a644a
commit e5f5f03
Showing
7 changed files
with
297 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@rainbow-me/rainbowkit": patch | ||
--- | ||
|
||
Removed external `i18n-js` dependency to reduce RainbowKit bundle sizes. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { describe, expect, it } from 'vitest'; | ||
import { I18n } from './I18n'; | ||
|
||
describe('I18n', () => { | ||
describe('t (translate)', () => { | ||
const testBasicTranslation = ( | ||
locale: string, | ||
translations: Record<string, string>, | ||
key: string, | ||
) => { | ||
const i18n = new I18n({ [locale]: translations }); | ||
i18n.locale = locale; | ||
|
||
it(`should translate '${locale}' locale for '${key}'`, () => { | ||
expect(i18n.t(key)).toBe(translations[key]); | ||
}); | ||
}; | ||
|
||
// Basic translation for 'hello' key | ||
testBasicTranslation('en-US', { hello: 'hello' }, 'hello'); | ||
testBasicTranslation('ru-RU', { hello: 'привет' }, 'hello'); | ||
testBasicTranslation('ja-JP', { hello: 'こんにちは' }, 'hello'); | ||
testBasicTranslation('ar-AR', { hello: 'مرحبًا' }, 'hello'); | ||
|
||
it("should translate 'en-US' if 'ja-JP' translation is missing (fallback enabled)", () => { | ||
const i18n = new I18n({ | ||
'en-US': { | ||
hello: 'hello', | ||
}, | ||
'ja-JP': { | ||
apple: 'りんご', | ||
}, | ||
}); | ||
|
||
i18n.enableFallback = true; | ||
// defaultLocale will be used | ||
// as fallback translation | ||
i18n.defaultLocale = 'en-US'; | ||
i18n.locale = 'ja-JP'; | ||
|
||
expect(i18n.t('hello')).toBe('hello'); | ||
}); | ||
|
||
it('should return missing message if translation does not exist', () => { | ||
const i18n = new I18n({ | ||
'ja-JP': { | ||
hello: 'こんにちは', | ||
}, | ||
}); | ||
|
||
i18n.locale = 'ja-JP'; | ||
|
||
expect(i18n.t('xyz')).toBe(`[missing: "ja-JP.xyz" translation]`); | ||
}); | ||
|
||
it('should return missing message if no locale present', () => { | ||
const i18n = new I18n({}); | ||
|
||
i18n.locale = 'ja-JP'; | ||
|
||
expect(i18n.t('xyz')).toBe(`[missing: "ja-JP.xyz" translation]`); | ||
}); | ||
|
||
it("should return missing message if 'ja-JP' has missing translation (fallback disabled)", () => { | ||
const i18n = new I18n({ | ||
'en-US': { | ||
hello: 'hello', | ||
}, | ||
'ja-JP': { | ||
apple: 'りんご', | ||
}, | ||
}); | ||
|
||
i18n.defaultLocale = 'en-US'; | ||
i18n.locale = 'ja-JP'; | ||
|
||
expect(i18n.t('hello')).toBe(`[missing: "ja-JP.hello" translation]`); | ||
}); | ||
|
||
it('should translate with replacement', () => { | ||
const i18n = new I18n({ | ||
'en-US': { | ||
hello: 'hello %{firstName} %{lastName}', | ||
}, | ||
}); | ||
|
||
i18n.locale = 'en-US'; | ||
|
||
expect(i18n.t('hello', { firstName: 'john', lastName: 'doe' })).toBe( | ||
'hello john doe', | ||
); | ||
}); | ||
}); | ||
|
||
describe('onChange', () => { | ||
it('should call onChange function if locale is updated', () => { | ||
const i18n = new I18n({ | ||
'en-US': { | ||
hello: 'hello', | ||
}, | ||
}); | ||
|
||
let called = false; | ||
|
||
i18n.onChange(() => { | ||
called = true; | ||
}); | ||
|
||
i18n.setTranslations('ru-RU', { | ||
hello: 'привет', | ||
}); | ||
|
||
expect(called).toBe(true); | ||
}); | ||
|
||
it('should unsubscribe onChange if cleanup function is called', () => { | ||
const i18n = new I18n({ | ||
'en-US': { | ||
hello: 'hello', | ||
}, | ||
}); | ||
|
||
let called = false; | ||
|
||
const unsubscribe = i18n.onChange(() => { | ||
called = true; | ||
}); | ||
|
||
unsubscribe(); // Unsubscribe immediately | ||
|
||
i18n.setTranslations('ru-RU', { | ||
hello: 'привет', | ||
}); | ||
|
||
expect(called).toBe(false); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
type GenericTranslationObject = Record<string, any>; | ||
|
||
const defaultOptions = { | ||
defaultLocale: 'en', | ||
locale: 'en', | ||
}; | ||
|
||
export class I18n { | ||
public listeners: Set<() => void> = new Set(); | ||
public defaultLocale = defaultOptions.defaultLocale; | ||
public enableFallback = false; | ||
public locale = defaultOptions.locale; | ||
private cachedLocales: string[] = []; | ||
public translations: GenericTranslationObject = {}; | ||
|
||
constructor(localeTranslations: Record<string, GenericTranslationObject>) { | ||
for (const [locale, translation] of Object.entries(localeTranslations)) { | ||
this.cachedLocales = [...this.cachedLocales, locale]; | ||
this.translations = { | ||
...this.translations, | ||
...this.flattenTranslation(translation, locale), | ||
}; | ||
} | ||
} | ||
|
||
private missingMessage(key: string): string { | ||
return `[missing: "${this.locale}.${key}" translation]`; | ||
} | ||
|
||
private flattenTranslation( | ||
translationObject: GenericTranslationObject, | ||
locale: string, | ||
): GenericTranslationObject { | ||
const result: GenericTranslationObject = {}; | ||
|
||
const flatten = ( | ||
currentTranslationObj: GenericTranslationObject, | ||
parentKey: string, | ||
) => { | ||
for (const key of Object.keys(currentTranslationObj)) { | ||
// Generate a new key for each iteration e.g 'en-US.connect.title' | ||
const newKey = `${parentKey}.${key}`; | ||
const currentValue = currentTranslationObj[key]; | ||
|
||
// If more nested values are encountered in the object, then | ||
// the same function will be called again | ||
if (typeof currentValue === 'object' && currentValue !== null) { | ||
flatten(currentValue, newKey); | ||
} else { | ||
// Otherwise, assign the result to the final | ||
// object value with the new key | ||
result[newKey] = currentValue; | ||
} | ||
} | ||
}; | ||
|
||
flatten(translationObject, locale); | ||
return result; | ||
} | ||
|
||
private translateWithReplacements( | ||
translation: string, | ||
replacements: Record<string, string> = {}, | ||
) { | ||
let translatedString = translation; | ||
for (const placeholder in replacements) { | ||
const replacementValue = replacements[placeholder]; | ||
translatedString = translatedString.replace( | ||
`%{${placeholder}}`, | ||
replacementValue, | ||
); | ||
} | ||
return translatedString; | ||
} | ||
|
||
public t(key: string, replacements?: Record<string, string>): string { | ||
const translationKey = `${this.locale}.${key}`; | ||
const translation = this.translations[translationKey]; | ||
|
||
if (!translation) { | ||
// If fallback is enabled | ||
if (this.enableFallback) { | ||
const fallbackTranslationKey = `${this.defaultLocale}.${key}`; | ||
const fallbackTranslation = this.translations[fallbackTranslationKey]; | ||
|
||
// If translation exist for the default | ||
// locale return it as a fallback translation | ||
if (fallbackTranslation) { | ||
return this.translateWithReplacements( | ||
fallbackTranslation, | ||
replacements, | ||
); | ||
} | ||
} | ||
|
||
return this.missingMessage(key); | ||
} | ||
|
||
return this.translateWithReplacements(translation, replacements); | ||
} | ||
|
||
public isLocaleCached(locale: string) { | ||
return this.cachedLocales.includes(locale); | ||
} | ||
|
||
public updateLocale(locale: string) { | ||
this.locale = locale; | ||
this.notifyListeners(); | ||
} | ||
|
||
public setTranslations( | ||
locale: string, | ||
translations: GenericTranslationObject, | ||
) { | ||
const cachedLocale = this.isLocaleCached(locale); | ||
|
||
if (!cachedLocale) { | ||
this.cachedLocales = [...this.cachedLocales, locale]; | ||
this.translations = { | ||
...this.translations, | ||
...this.flattenTranslation(translations, locale), | ||
}; | ||
} | ||
|
||
this.locale = locale; | ||
|
||
this.notifyListeners(); | ||
} | ||
|
||
private notifyListeners(): void { | ||
for (const listener of this.listeners) { | ||
listener(); | ||
} | ||
} | ||
|
||
public onChange(fn: () => void): () => void { | ||
this.listeners.add(fn); | ||
|
||
return () => { | ||
this.listeners.delete(fn); | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.