[WEB-3153] improvement: add support for nested translations and ICU formatting (#6411)
* improvement: add support for nested translations and ICU formatting * chore: comment update
This commit is contained in:
parent
3ac20741d9
commit
59ddc02a31
15 changed files with 307 additions and 134 deletions
|
|
@ -10,7 +10,8 @@
|
|||
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
|
||||
},
|
||||
"dependencies": {
|
||||
"@plane/utils": "*"
|
||||
"@plane/utils": "*",
|
||||
"intl-messageformat": "^10.7.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "*",
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import { observer } from "mobx-react";
|
||||
import React, { createContext, useEffect } from "react";
|
||||
import { Language, languages } from "../config";
|
||||
import { TranslationStore } from "./store";
|
||||
|
||||
// Create the store instance
|
||||
const translationStore = new TranslationStore();
|
||||
|
||||
// Create Context
|
||||
export const TranslationContext = createContext<TranslationStore>(translationStore);
|
||||
|
||||
export const TranslationProvider = observer(({ children }: { children: React.ReactNode }) => {
|
||||
// Handle storage events for cross-tab synchronization
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (event: StorageEvent) => {
|
||||
if (event.key === "userLanguage" && event.newValue) {
|
||||
const newLang = event.newValue as Language;
|
||||
if (languages.includes(newLang)) {
|
||||
translationStore.setLanguage(newLang);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
return () => window.removeEventListener("storage", handleStorageChange);
|
||||
}, []);
|
||||
|
||||
return <TranslationContext.Provider value={translationStore}>{children}</TranslationContext.Provider>;
|
||||
});
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { makeObservable, observable } from "mobx";
|
||||
import { Language, fallbackLng, languages, translations } from "../config";
|
||||
|
||||
export class TranslationStore {
|
||||
currentLocale: Language = fallbackLng;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
currentLocale: observable.ref,
|
||||
});
|
||||
this.initializeLanguage();
|
||||
}
|
||||
|
||||
get availableLanguages() {
|
||||
return languages;
|
||||
}
|
||||
|
||||
t(key: string) {
|
||||
return translations[this.currentLocale]?.[key] || translations[fallbackLng][key] || key;
|
||||
}
|
||||
|
||||
setLanguage(lng: Language) {
|
||||
try {
|
||||
localStorage.setItem("userLanguage", lng);
|
||||
this.currentLocale = lng;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
initializeLanguage() {
|
||||
if (typeof window === "undefined") return;
|
||||
const savedLocale = localStorage.getItem("userLanguage") as Language;
|
||||
if (savedLocale && languages.includes(savedLocale)) {
|
||||
this.setLanguage(savedLocale);
|
||||
} else {
|
||||
const browserLang = navigator.language.split("-")[0] as Language;
|
||||
const newLocale = languages.includes(browserLang as Language) ? (browserLang as Language) : fallbackLng;
|
||||
this.setLanguage(newLocale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import en from "../locales/en/translations.json";
|
||||
import es from "../locales/es/translations.json";
|
||||
import fr from "../locales/fr/translations.json";
|
||||
import ja from "../locales/ja/translations.json";
|
||||
import zh_CN from "../locales/zh-CN/translations.json";
|
||||
|
||||
export type Language = (typeof languages)[number];
|
||||
export type Translations = {
|
||||
[key: string]: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const fallbackLng = "en";
|
||||
export const languages = ["en", "fr", "es", "ja", "zh-CN"] as const;
|
||||
export const translations: Translations = {
|
||||
en,
|
||||
fr,
|
||||
es,
|
||||
ja,
|
||||
zh_CN,
|
||||
};
|
||||
|
||||
export const SUPPORTED_LANGUAGES = [
|
||||
{
|
||||
label: "English",
|
||||
value: "en",
|
||||
},
|
||||
{
|
||||
label: "French",
|
||||
value: "fr",
|
||||
},
|
||||
{
|
||||
label: "Spanish",
|
||||
value: "es",
|
||||
},
|
||||
{
|
||||
label: "Japanese",
|
||||
value: "ja",
|
||||
},
|
||||
{
|
||||
label: "Chinese",
|
||||
value: "zh-CN",
|
||||
},
|
||||
];
|
||||
1
packages/i18n/src/constants/index.ts
Normal file
1
packages/i18n/src/constants/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./language";
|
||||
13
packages/i18n/src/constants/language.ts
Normal file
13
packages/i18n/src/constants/language.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { TLanguage, ILanguageOption } from "../types";
|
||||
|
||||
export const FALLBACK_LANGUAGE: TLanguage = "en";
|
||||
|
||||
export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
|
||||
{ label: "English", value: "en" },
|
||||
{ label: "Français", value: "fr" },
|
||||
{ label: "Español", value: "es" },
|
||||
{ label: "日本語", value: "ja" },
|
||||
{ label: "中文", value: "zh-CN" },
|
||||
];
|
||||
|
||||
export const STORAGE_KEY = "userLanguage";
|
||||
19
packages/i18n/src/context/index.tsx
Normal file
19
packages/i18n/src/context/index.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { observer } from "mobx-react";
|
||||
import React, { createContext } from "react";
|
||||
// store
|
||||
import { TranslationStore } from "../store";
|
||||
|
||||
export const TranslationContext = createContext<TranslationStore | null>(null);
|
||||
|
||||
interface TranslationProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the translation store to the application
|
||||
*/
|
||||
export const TranslationProvider: React.FC<TranslationProviderProps> = observer(({ children }) => {
|
||||
const [store] = React.useState(() => new TranslationStore());
|
||||
|
||||
return <TranslationContext.Provider value={store}>{children}</TranslationContext.Provider>;
|
||||
});
|
||||
|
|
@ -1,17 +1,35 @@
|
|||
import { useContext } from "react";
|
||||
import { TranslationContext } from "../components";
|
||||
import { Language } from "../config";
|
||||
import { useContext } from 'react';
|
||||
// context
|
||||
import { TranslationContext } from '../context';
|
||||
// types
|
||||
import { ILanguageOption, TLanguage } from '../types';
|
||||
|
||||
export function useTranslation() {
|
||||
export type TTranslationStore = {
|
||||
t: (key: string, params?: Record<string, any>) => string;
|
||||
currentLocale: TLanguage;
|
||||
changeLanguage: (lng: TLanguage) => void;
|
||||
languages: ILanguageOption[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides the translation store to the application
|
||||
* @returns {TTranslationStore}
|
||||
* @returns {(key: string, params?: Record<string, any>) => string} t: method to translate the key with params
|
||||
* @returns {TLanguage} currentLocale - current locale language
|
||||
* @returns {(lng: TLanguage) => void} changeLanguage - method to change the language
|
||||
* @returns {ILanguageOption[]} languages - available languages
|
||||
* @throws {Error} if the TranslationProvider is not used
|
||||
*/
|
||||
export function useTranslation(): TTranslationStore {
|
||||
const store = useContext(TranslationContext);
|
||||
if (!store) {
|
||||
throw new Error("useTranslation must be used within a TranslationProvider");
|
||||
throw new Error('useTranslation must be used within a TranslationProvider');
|
||||
}
|
||||
|
||||
return {
|
||||
t: (key: string) => store.t(key),
|
||||
t: store.t.bind(store),
|
||||
currentLocale: store.currentLocale,
|
||||
changeLanguage: (lng: Language) => store.setLanguage(lng),
|
||||
changeLanguage: (lng: TLanguage) => store.setLanguage(lng),
|
||||
languages: store.availableLanguages,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./config";
|
||||
export * from "./components";
|
||||
export * from "./constants";
|
||||
export * from "./context";
|
||||
export * from "./hooks";
|
||||
export * from "./types";
|
||||
|
|
|
|||
170
packages/i18n/src/store/index.ts
Normal file
170
packages/i18n/src/store/index.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import IntlMessageFormat from "intl-messageformat";
|
||||
import get from "lodash/get";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
// constants
|
||||
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, STORAGE_KEY } from "../constants";
|
||||
// types
|
||||
import { TLanguage, ILanguageOption, ITranslations } from "../types";
|
||||
|
||||
/**
|
||||
* Mobx store class for handling translations and language changes in the application
|
||||
* Provides methods to translate keys with params and change the language
|
||||
* Uses IntlMessageFormat to format the translations
|
||||
*/
|
||||
export class TranslationStore {
|
||||
// List of translations for each language
|
||||
private translations: ITranslations = {};
|
||||
// Cache for IntlMessageFormat instances
|
||||
private messageCache: Map<string, IntlMessageFormat> = new Map();
|
||||
// Current language
|
||||
currentLocale: TLanguage = FALLBACK_LANGUAGE;
|
||||
|
||||
/**
|
||||
* Constructor for the TranslationStore class
|
||||
*/
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
this.initializeLanguage();
|
||||
this.loadTranslations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads translations from JSON files and initializes the message cache
|
||||
*/
|
||||
private async loadTranslations() {
|
||||
try {
|
||||
// dynamic import of translations
|
||||
const translations = {
|
||||
en: (await import("../locales/en/translations.json")).default,
|
||||
fr: (await import("../locales/fr/translations.json")).default,
|
||||
es: (await import("../locales/es/translations.json")).default,
|
||||
ja: (await import("../locales/ja/translations.json")).default,
|
||||
"zh-CN": (await import("../locales/zh-CN/translations.json")).default,
|
||||
};
|
||||
this.translations = translations;
|
||||
this.messageCache.clear(); // Clear cache when translations change
|
||||
} catch (error) {
|
||||
console.error("Failed to load translations:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Initializes the language based on the local storage or browser language */
|
||||
private initializeLanguage() {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const savedLocale = localStorage.getItem(STORAGE_KEY) as TLanguage;
|
||||
if (this.isValidLanguage(savedLocale)) {
|
||||
this.setLanguage(savedLocale);
|
||||
return;
|
||||
}
|
||||
|
||||
const browserLang = this.getBrowserLanguage();
|
||||
this.setLanguage(browserLang);
|
||||
}
|
||||
|
||||
/** Checks if the language is valid based on the supported languages */
|
||||
private isValidLanguage(lang: string | null): lang is TLanguage {
|
||||
return lang !== null && SUPPORTED_LANGUAGES.some((l) => l.value === lang);
|
||||
}
|
||||
|
||||
/** Gets the browser language based on the navigator.language */
|
||||
private getBrowserLanguage(): TLanguage {
|
||||
const browserLang = navigator.language.split("-")[0];
|
||||
return this.isValidLanguage(browserLang) ? browserLang : FALLBACK_LANGUAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cache key for the given key and locale
|
||||
* @param key - the key to get the cache key for
|
||||
* @param locale - the locale to get the cache key for
|
||||
* @returns the cache key for the given key and locale
|
||||
*/
|
||||
private getCacheKey(key: string, locale: TLanguage): string {
|
||||
return `${locale}:${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the IntlMessageFormat instance for the given key and locale
|
||||
* Returns cached instance if available
|
||||
* Throws an error if the key is not found in the translations
|
||||
*/
|
||||
private getMessageInstance(key: string, locale: TLanguage): IntlMessageFormat | null {
|
||||
const cacheKey = this.getCacheKey(key, locale);
|
||||
|
||||
// Check if the cache already has the key
|
||||
if (this.messageCache.has(cacheKey)) {
|
||||
return this.messageCache.get(cacheKey) || null;
|
||||
}
|
||||
|
||||
// Get the message from the translations
|
||||
const message = get(this.translations[locale], key);
|
||||
if (!message) return null;
|
||||
|
||||
try {
|
||||
const formatter = new IntlMessageFormat(message as any, locale);
|
||||
this.messageCache.set(cacheKey, formatter);
|
||||
return formatter;
|
||||
} catch (error) {
|
||||
console.error(`Failed to create message formatter for key "${key}":`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a key with params using the current locale
|
||||
* Falls back to the default language if the translation is not found
|
||||
* Returns the key itself if the translation is not found
|
||||
* @param key - The key to translate
|
||||
* @param params - The params to format the translation with
|
||||
* @returns The translated string
|
||||
*/
|
||||
t(key: string, params?: Record<string, any>): string {
|
||||
try {
|
||||
// Try current locale
|
||||
let formatter = this.getMessageInstance(key, this.currentLocale);
|
||||
|
||||
// Fallback to default language if necessary
|
||||
if (!formatter && this.currentLocale !== FALLBACK_LANGUAGE) {
|
||||
formatter = this.getMessageInstance(key, FALLBACK_LANGUAGE);
|
||||
}
|
||||
|
||||
// If we have a formatter, use it
|
||||
if (formatter) {
|
||||
return formatter.format(params || {}) as string;
|
||||
}
|
||||
|
||||
// Last resort: return the key itself
|
||||
return key;
|
||||
} catch (error) {
|
||||
console.error(`Translation error for key "${key}":`, error);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current language and updates the translations
|
||||
* @param lng - The new language
|
||||
*/
|
||||
setLanguage(lng: TLanguage): void {
|
||||
try {
|
||||
if (!this.isValidLanguage(lng)) {
|
||||
throw new Error(`Invalid language: ${lng}`);
|
||||
}
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, lng);
|
||||
this.currentLocale = lng;
|
||||
this.messageCache.clear(); // Clear cache when language changes
|
||||
document.documentElement.lang = lng;
|
||||
} catch (error) {
|
||||
console.error("Failed to set language:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the available language options for the dropdown
|
||||
* @returns An array of language options
|
||||
*/
|
||||
get availableLanguages(): ILanguageOption[] {
|
||||
return SUPPORTED_LANGUAGES;
|
||||
}
|
||||
}
|
||||
2
packages/i18n/src/types/index.ts
Normal file
2
packages/i18n/src/types/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./language";
|
||||
export * from "./translation";
|
||||
6
packages/i18n/src/types/language.ts
Normal file
6
packages/i18n/src/types/language.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN";
|
||||
|
||||
export interface ILanguageOption {
|
||||
label: string;
|
||||
value: TLanguage;
|
||||
}
|
||||
7
packages/i18n/src/types/translation.ts
Normal file
7
packages/i18n/src/types/translation.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface ITranslation {
|
||||
[key: string]: string | ITranslation;
|
||||
}
|
||||
|
||||
export interface ITranslations {
|
||||
[locale: string]: ITranslation;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { ReactNode, useEffect, FC } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTranslation, Language } from "@plane/i18n";
|
||||
import { useTranslation, TLanguage } from "@plane/i18n";
|
||||
// helpers
|
||||
import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper";
|
||||
// hooks
|
||||
|
|
@ -53,7 +53,7 @@ const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (!userProfile?.language) return;
|
||||
changeLanguage(userProfile?.language as Language);
|
||||
changeLanguage(userProfile?.language as TLanguage);
|
||||
}, [userProfile?.language, changeLanguage]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
63
yarn.lock
63
yarn.lock
|
|
@ -1397,6 +1397,47 @@
|
|||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429"
|
||||
integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==
|
||||
|
||||
"@formatjs/ecma402-abstract@2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz#0ee291effe7ee2c340742a6c95d92eacb5e6c00a"
|
||||
integrity sha512-6sE5nyvDloULiyOMbOTJEEgWL32w+VHkZQs8S02Lnn8Y/O5aQhjOEXwWzvR7SsBE/exxlSpY2EsWZgqHbtLatg==
|
||||
dependencies:
|
||||
"@formatjs/fast-memoize" "2.2.6"
|
||||
"@formatjs/intl-localematcher" "0.5.10"
|
||||
decimal.js "10"
|
||||
tslib "2"
|
||||
|
||||
"@formatjs/fast-memoize@2.2.6":
|
||||
version "2.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.6.tgz#fac0a84207a1396be1f1aa4ee2805b179e9343d1"
|
||||
integrity sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==
|
||||
dependencies:
|
||||
tslib "2"
|
||||
|
||||
"@formatjs/icu-messageformat-parser@2.9.8":
|
||||
version "2.9.8"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.8.tgz#118e7156f8a8db6b27b650f09334db21456c681f"
|
||||
integrity sha512-hZlLNI3+Lev8IAXuwehLoN7QTKqbx3XXwFW1jh0AdIA9XJdzn9Uzr+2LLBspPm/PX0+NLIfykj/8IKxQqHUcUQ==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "2.3.2"
|
||||
"@formatjs/icu-skeleton-parser" "1.8.12"
|
||||
tslib "2"
|
||||
|
||||
"@formatjs/icu-skeleton-parser@1.8.12":
|
||||
version "1.8.12"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.12.tgz#43076747cdbe0f23bfac2b2a956bd8219716680d"
|
||||
integrity sha512-QRAY2jC1BomFQHYDMcZtClqHR55EEnB96V7Xbk/UiBodsuFc5kujybzt87+qj1KqmJozFhk6n4KiT1HKwAkcfg==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "2.3.2"
|
||||
tslib "2"
|
||||
|
||||
"@formatjs/intl-localematcher@0.5.10":
|
||||
version "0.5.10"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz#1e0bd3fc1332c1fe4540cfa28f07e9227b659a58"
|
||||
integrity sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==
|
||||
dependencies:
|
||||
tslib "2"
|
||||
|
||||
"@headlessui/react@^1.7.13", "@headlessui/react@^1.7.19", "@headlessui/react@^1.7.3":
|
||||
version "1.7.19"
|
||||
resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz"
|
||||
|
|
@ -6027,7 +6068,7 @@ decimal.js-light@^2.4.1:
|
|||
resolved "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz"
|
||||
integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
|
||||
|
||||
decimal.js@^10.4.3:
|
||||
decimal.js@10, decimal.js@^10.4.3:
|
||||
version "10.4.3"
|
||||
resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz"
|
||||
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
|
||||
|
|
@ -7993,6 +8034,16 @@ internmap@^1.0.0:
|
|||
resolved "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz"
|
||||
integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==
|
||||
|
||||
intl-messageformat@^10.7.11:
|
||||
version "10.7.11"
|
||||
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.11.tgz#f24893b2a64e7b5ec29f9eceb4f1a58bde1346e0"
|
||||
integrity sha512-IB2N1tmI24k2EFH3PWjU7ivJsnWyLwOWOva0jnXFa29WzB6fb0JZ5EMQGu+XN5lDtjHYFo0/UooP67zBwUg7rQ==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "2.3.2"
|
||||
"@formatjs/fast-memoize" "2.2.6"
|
||||
"@formatjs/icu-messageformat-parser" "2.9.8"
|
||||
tslib "2"
|
||||
|
||||
invariant@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz"
|
||||
|
|
@ -12303,16 +12354,16 @@ tsconfig-paths@^4.2.0:
|
|||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.7.0, tslib@^2.8.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
tslib@^1.8.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.7.0, tslib@^2.8.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
tslib@~2.5.0:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue