mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-01 10:22:30 +02:00
feat(v2): infer default i18n locale config from locale code (#4449)
* improve locale default config * remove localeConfigs from i18n tutorial * better i18n types, tests and Intl.DisplayNames integration
This commit is contained in:
parent
5e73c72f26
commit
806fdbaf27
10 changed files with 189 additions and 110 deletions
|
@ -39,6 +39,7 @@ export default function LocaleDropdownNavbarItem({
|
||||||
target: '_self',
|
target: '_self',
|
||||||
autoAddBaseUrl: false,
|
autoAddBaseUrl: false,
|
||||||
className: locale === currentLocale ? 'dropdown__link--active' : '',
|
className: locale === currentLocale ? 'dropdown__link--active' : '',
|
||||||
|
style: {textTransform: 'capitalize'},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
7
packages/docusaurus-types/src/index.d.ts
vendored
7
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -101,11 +101,14 @@ export type I18nLocaleConfig = {
|
||||||
export type I18nConfig = {
|
export type I18nConfig = {
|
||||||
defaultLocale: string;
|
defaultLocale: string;
|
||||||
locales: [string, ...string[]];
|
locales: [string, ...string[]];
|
||||||
localeConfigs: Record<string, I18nLocaleConfig>;
|
localeConfigs: Record<string, Partial<I18nLocaleConfig>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type I18n = I18nConfig & {
|
export type I18n = {
|
||||||
|
defaultLocale: string;
|
||||||
|
locales: [string, ...string[]];
|
||||||
currentLocale: string;
|
currentLocale: string;
|
||||||
|
localeConfigs: Record<string, I18nLocaleConfig>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface DocusaurusContext {
|
export interface DocusaurusContext {
|
||||||
|
|
|
@ -102,6 +102,7 @@
|
||||||
"react-router-config": "^5.1.1",
|
"react-router-config": "^5.1.1",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"resolve-pathname": "^3.0.0",
|
"resolve-pathname": "^3.0.0",
|
||||||
|
"rtl-detect": "^1.0.2",
|
||||||
"semver": "^7.3.4",
|
"semver": "^7.3.4",
|
||||||
"serve-handler": "^6.1.3",
|
"serve-handler": "^6.1.3",
|
||||||
"shelljs": "^0.8.4",
|
"shelljs": "^0.8.4",
|
||||||
|
|
|
@ -5,6 +5,11 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export const NODE_MAJOR_VERSION = parseInt(
|
||||||
|
process.versions.node.split('.')[0],
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
// Can be overridden with cli option --out-dir
|
// Can be overridden with cli option --out-dir
|
||||||
export const DEFAULT_BUILD_DIR_NAME = 'build';
|
export const DEFAULT_BUILD_DIR_NAME = 'build';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`loadI18n should throw when trying to load undeclared locale 1`] = `
|
|
||||||
"It is not possible to load Docusaurus with locale=\\"it\\".
|
|
||||||
This locale is not in the available locales of your site configuration: config.i18n.locales=[en,fr,de]
|
|
||||||
Note: Docusaurus only support running one locale at a time."
|
|
||||||
`;
|
|
|
@ -5,25 +5,100 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {loadI18n, localizePath, defaultLocaleConfig} from '../i18n';
|
import {
|
||||||
|
loadI18n,
|
||||||
|
localizePath,
|
||||||
|
getDefaultLocaleConfig,
|
||||||
|
shouldWarnAboutNodeVersion,
|
||||||
|
} from '../i18n';
|
||||||
import {DEFAULT_I18N_CONFIG} from '../configValidation';
|
import {DEFAULT_I18N_CONFIG} from '../configValidation';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {chain, identity} from 'lodash';
|
import {chain, identity} from 'lodash';
|
||||||
|
import {I18nConfig} from '@docusaurus/types';
|
||||||
|
|
||||||
function testLocaleConfigsFor(locales: string[]) {
|
function testLocaleConfigsFor(locales: string[]) {
|
||||||
return chain(locales).keyBy(identity).mapValues(defaultLocaleConfig).value();
|
return chain(locales)
|
||||||
|
.keyBy(identity)
|
||||||
|
.mapValues(getDefaultLocaleConfig)
|
||||||
|
.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadI18nTest(i18nConfig: I18nConfig, locale?: string) {
|
||||||
|
return loadI18n(
|
||||||
|
// @ts-expect-error: enough for this test
|
||||||
|
{
|
||||||
|
i18n: i18nConfig,
|
||||||
|
},
|
||||||
|
{locale},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('defaultLocaleConfig', () => {
|
||||||
|
// @ts-expect-error: wait for TS support of ES2021 feature
|
||||||
|
const canComputeLabel = typeof Intl.DisplayNames !== 'undefined';
|
||||||
|
|
||||||
|
test('returns correct labels', () => {
|
||||||
|
expect(getDefaultLocaleConfig('fr')).toEqual({
|
||||||
|
label: canComputeLabel ? 'français' : 'fr',
|
||||||
|
direction: 'ltr',
|
||||||
|
});
|
||||||
|
expect(getDefaultLocaleConfig('fr-FR')).toEqual({
|
||||||
|
label: canComputeLabel ? 'français (France)' : 'fr-FR',
|
||||||
|
direction: 'ltr',
|
||||||
|
});
|
||||||
|
expect(getDefaultLocaleConfig('en')).toEqual({
|
||||||
|
label: canComputeLabel ? 'English' : 'en',
|
||||||
|
direction: 'ltr',
|
||||||
|
});
|
||||||
|
expect(getDefaultLocaleConfig('en-US')).toEqual({
|
||||||
|
label: canComputeLabel ? 'American English' : 'en-US',
|
||||||
|
direction: 'ltr',
|
||||||
|
});
|
||||||
|
expect(getDefaultLocaleConfig('zh')).toEqual({
|
||||||
|
label: canComputeLabel ? '中文' : 'zh',
|
||||||
|
direction: 'ltr',
|
||||||
|
});
|
||||||
|
expect(getDefaultLocaleConfig('zh-CN')).toEqual({
|
||||||
|
label: canComputeLabel ? '中文(中国)' : 'zh-CN',
|
||||||
|
direction: 'ltr',
|
||||||
|
});
|
||||||
|
expect(getDefaultLocaleConfig('en-US')).toEqual({
|
||||||
|
label: canComputeLabel ? 'American English' : 'en-US',
|
||||||
|
direction: 'ltr',
|
||||||
|
});
|
||||||
|
expect(getDefaultLocaleConfig('fa')).toEqual({
|
||||||
|
label: canComputeLabel ? 'فارسی' : 'fa',
|
||||||
|
direction: 'rtl',
|
||||||
|
});
|
||||||
|
expect(getDefaultLocaleConfig('fa-IR')).toEqual({
|
||||||
|
label: canComputeLabel ? 'فارسی (ایران)' : 'fa-IR',
|
||||||
|
direction: 'rtl',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shouldWarnAboutNodeVersion', () => {
|
||||||
|
test('warns for old NodeJS version and [en,fr]', () => {
|
||||||
|
expect(shouldWarnAboutNodeVersion(12, ['en', 'fr'])).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('not warn for old NodeJS version and [en]', () => {
|
||||||
|
expect(shouldWarnAboutNodeVersion(12, ['en'])).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('not warn for recent NodeJS version and [en,fr]', () => {
|
||||||
|
expect(shouldWarnAboutNodeVersion(14, ['en', 'fr'])).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('loadI18n', () => {
|
describe('loadI18n', () => {
|
||||||
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleSpy.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
test('should load I18n for default config', async () => {
|
test('should load I18n for default config', async () => {
|
||||||
await expect(
|
await expect(loadI18nTest(DEFAULT_I18N_CONFIG)).resolves.toEqual({
|
||||||
loadI18n(
|
|
||||||
// @ts-expect-error: enough for this test
|
|
||||||
{
|
|
||||||
i18n: DEFAULT_I18N_CONFIG,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).resolves.toEqual({
|
|
||||||
defaultLocale: 'en',
|
defaultLocale: 'en',
|
||||||
locales: ['en'],
|
locales: ['en'],
|
||||||
currentLocale: 'en',
|
currentLocale: 'en',
|
||||||
|
@ -33,16 +108,11 @@ describe('loadI18n', () => {
|
||||||
|
|
||||||
test('should load I18n for multi-lang config', async () => {
|
test('should load I18n for multi-lang config', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
loadI18n(
|
loadI18nTest({
|
||||||
// @ts-expect-error: enough for this test
|
defaultLocale: 'fr',
|
||||||
{
|
locales: ['en', 'fr', 'de'],
|
||||||
i18n: {
|
localeConfigs: {},
|
||||||
defaultLocale: 'fr',
|
}),
|
||||||
locales: ['en', 'fr', 'de'],
|
|
||||||
localeConfigs: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
defaultLocale: 'fr',
|
defaultLocale: 'fr',
|
||||||
locales: ['en', 'fr', 'de'],
|
locales: ['en', 'fr', 'de'],
|
||||||
|
@ -53,16 +123,13 @@ describe('loadI18n', () => {
|
||||||
|
|
||||||
test('should load I18n for multi-locale config with specified locale', async () => {
|
test('should load I18n for multi-locale config with specified locale', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
loadI18n(
|
loadI18nTest(
|
||||||
// @ts-expect-error: enough for this test
|
|
||||||
{
|
{
|
||||||
i18n: {
|
defaultLocale: 'fr',
|
||||||
defaultLocale: 'fr',
|
locales: ['en', 'fr', 'de'],
|
||||||
locales: ['en', 'fr', 'de'],
|
localeConfigs: {},
|
||||||
localeConfigs: {},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{locale: 'de'},
|
'de',
|
||||||
),
|
),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
defaultLocale: 'fr',
|
defaultLocale: 'fr',
|
||||||
|
@ -74,19 +141,16 @@ describe('loadI18n', () => {
|
||||||
|
|
||||||
test('should load I18n for multi-locale config with some xcustom locale configs', async () => {
|
test('should load I18n for multi-locale config with some xcustom locale configs', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
loadI18n(
|
loadI18nTest(
|
||||||
{
|
{
|
||||||
i18n: {
|
defaultLocale: 'fr',
|
||||||
defaultLocale: 'fr',
|
locales: ['en', 'fr', 'de'],
|
||||||
locales: ['en', 'fr', 'de'],
|
localeConfigs: {
|
||||||
localeConfigs: {
|
fr: {label: 'Français'},
|
||||||
fr: {label: 'Français'},
|
en: {},
|
||||||
// @ts-expect-error: empty on purpose
|
|
||||||
en: {},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{locale: 'de'},
|
'de',
|
||||||
),
|
),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
defaultLocale: 'fr',
|
defaultLocale: 'fr',
|
||||||
|
@ -94,26 +158,24 @@ describe('loadI18n', () => {
|
||||||
currentLocale: 'de',
|
currentLocale: 'de',
|
||||||
localeConfigs: {
|
localeConfigs: {
|
||||||
fr: {label: 'Français', direction: 'ltr'},
|
fr: {label: 'Français', direction: 'ltr'},
|
||||||
en: defaultLocaleConfig('en'),
|
en: getDefaultLocaleConfig('en'),
|
||||||
de: defaultLocaleConfig('de'),
|
de: getDefaultLocaleConfig('de'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw when trying to load undeclared locale', async () => {
|
test('should warn when trying to load undeclared locale', async () => {
|
||||||
await expect(
|
await loadI18nTest(
|
||||||
loadI18n(
|
{
|
||||||
// @ts-expect-error: enough for this test
|
defaultLocale: 'fr',
|
||||||
{
|
locales: ['en', 'fr', 'de'],
|
||||||
i18n: {
|
localeConfigs: {},
|
||||||
defaultLocale: 'fr',
|
},
|
||||||
locales: ['en', 'fr', 'de'],
|
'it',
|
||||||
localeConfigs: {},
|
);
|
||||||
},
|
expect(consoleSpy.mock.calls[0][0]).toMatch(
|
||||||
},
|
/The locale=it was not found in your site configuration/,
|
||||||
{locale: 'it'},
|
);
|
||||||
),
|
|
||||||
).rejects.toThrowErrorMatchingSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,45 +7,79 @@
|
||||||
import {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types';
|
import {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {normalizeUrl} from '@docusaurus/utils';
|
import {normalizeUrl} from '@docusaurus/utils';
|
||||||
|
import {getLangDir} from 'rtl-detect';
|
||||||
|
import {NODE_MAJOR_VERSION} from '../constants';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
export function defaultLocaleConfig(locale: string): I18nLocaleConfig {
|
function getDefaultLocaleLabel(locale: string) {
|
||||||
|
// Intl.DisplayNames is ES2021 - Node14+
|
||||||
|
// https://v8.dev/features/intl-displaynames
|
||||||
|
// @ts-expect-error: wait for TS support of ES2021 feature
|
||||||
|
if (typeof Intl.DisplayNames !== 'undefined') {
|
||||||
|
// @ts-expect-error: wait for TS support of ES2021 feature
|
||||||
|
return new Intl.DisplayNames([locale], {type: 'language'}).of(locale);
|
||||||
|
}
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultLocaleConfig(locale: string): I18nLocaleConfig {
|
||||||
return {
|
return {
|
||||||
label: locale,
|
label: getDefaultLocaleLabel(locale),
|
||||||
direction: 'ltr',
|
direction: getLangDir(locale),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldWarnAboutNodeVersion(version: number, locales: string[]) {
|
||||||
|
const isOnlyEnglish = locales.length === 1 && locales.includes('en');
|
||||||
|
const isOlderNodeVersion = version < 14;
|
||||||
|
return isOlderNodeVersion && !isOnlyEnglish;
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadI18n(
|
export async function loadI18n(
|
||||||
config: DocusaurusConfig,
|
config: DocusaurusConfig,
|
||||||
options: {locale?: string} = {},
|
options: {locale?: string} = {},
|
||||||
): Promise<I18n> {
|
): Promise<I18n> {
|
||||||
const i18nConfig = config.i18n;
|
const {i18n: i18nConfig} = config;
|
||||||
|
|
||||||
const currentLocale = options.locale ?? i18nConfig.defaultLocale;
|
const currentLocale = options.locale ?? i18nConfig.defaultLocale;
|
||||||
|
|
||||||
if (currentLocale && !i18nConfig.locales.includes(currentLocale)) {
|
if (!i18nConfig.locales.includes(currentLocale)) {
|
||||||
throw new Error(
|
console.warn(
|
||||||
`It is not possible to load Docusaurus with locale="${currentLocale}".
|
chalk.yellow(
|
||||||
This locale is not in the available locales of your site configuration: config.i18n.locales=[${i18nConfig.locales.join(
|
`The locale=${currentLocale} was not found in your site configuration: config.i18n.locales=[${i18nConfig.locales.join(
|
||||||
',',
|
',',
|
||||||
)}]
|
)}]
|
||||||
Note: Docusaurus only support running one locale at a time.`,
|
Note: Docusaurus only support running one locale at a time.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const locales = i18nConfig.locales.includes(currentLocale)
|
||||||
|
? i18nConfig.locales
|
||||||
|
: (i18nConfig.locales.concat(currentLocale) as [string, ...string[]]);
|
||||||
|
|
||||||
|
if (shouldWarnAboutNodeVersion(NODE_MAJOR_VERSION, locales)) {
|
||||||
|
console.warn(
|
||||||
|
chalk.yellow(
|
||||||
|
`To use Docusaurus i18n, it is strongly advised to use NodeJS >= 14 (instead of ${NODE_MAJOR_VERSION})`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLocaleConfig(locale: string): I18nLocaleConfig {
|
function getLocaleConfig(locale: string): I18nLocaleConfig {
|
||||||
// User provided values
|
return {
|
||||||
const localeConfigOptions: Partial<I18nLocaleConfig> =
|
...getDefaultLocaleConfig(locale),
|
||||||
i18nConfig.localeConfigs[locale];
|
...i18nConfig.localeConfigs[locale],
|
||||||
|
};
|
||||||
return {...defaultLocaleConfig(locale), ...localeConfigOptions};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const localeConfigs = i18nConfig.locales.reduce((acc, locale) => {
|
const localeConfigs = locales.reduce((acc, locale) => {
|
||||||
return {...acc, [locale]: getLocaleConfig(locale)};
|
return {...acc, [locale]: getLocaleConfig(locale)};
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...i18nConfig,
|
defaultLocale: i18nConfig.defaultLocale,
|
||||||
|
locales,
|
||||||
currentLocale,
|
currentLocale,
|
||||||
localeConfigs,
|
localeConfigs,
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,14 +24,6 @@ module.exports = {
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: 'en',
|
defaultLocale: 'en',
|
||||||
locales: ['en', 'fr'],
|
locales: ['en', 'fr'],
|
||||||
localeConfigs: {
|
|
||||||
en: {
|
|
||||||
label: 'English',
|
|
||||||
},
|
|
||||||
fr: {
|
|
||||||
label: 'Français',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
|
@ -46,26 +46,6 @@ const isVersioningDisabled = !!process.env.DISABLE_VERSIONING;
|
||||||
// https://docusaurus-i18n-staging.netlify.app/
|
// https://docusaurus-i18n-staging.netlify.app/
|
||||||
const isI18nStaging = process.env.I18N_STAGING === 'true';
|
const isI18nStaging = process.env.I18N_STAGING === 'true';
|
||||||
|
|
||||||
const LocaleConfigs = isI18nStaging
|
|
||||||
? // Staging locales (https://docusaurus-i18n-staging.netlify.app/)
|
|
||||||
{
|
|
||||||
en: {
|
|
||||||
label: 'English',
|
|
||||||
},
|
|
||||||
'zh-CN': {
|
|
||||||
label: '简体中文',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: // Production locales
|
|
||||||
{
|
|
||||||
en: {
|
|
||||||
label: 'English',
|
|
||||||
},
|
|
||||||
fr: {
|
|
||||||
label: 'Français',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {import('@docusaurus/types').DocusaurusConfig} */
|
/** @type {import('@docusaurus/types').DocusaurusConfig} */
|
||||||
(module.exports = {
|
(module.exports = {
|
||||||
title: 'Docusaurus',
|
title: 'Docusaurus',
|
||||||
|
@ -77,8 +57,11 @@ const LocaleConfigs = isI18nStaging
|
||||||
url: 'https://v2.docusaurus.io',
|
url: 'https://v2.docusaurus.io',
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: 'en',
|
defaultLocale: 'en',
|
||||||
locales: Object.keys(LocaleConfigs),
|
locales: isI18nStaging
|
||||||
localeConfigs: LocaleConfigs,
|
? // Staging locales (https://docusaurus-i18n-staging.netlify.app/)
|
||||||
|
['en', 'zh-CN']
|
||||||
|
: // Production locales
|
||||||
|
['en', 'fr'],
|
||||||
},
|
},
|
||||||
onBrokenLinks: 'throw',
|
onBrokenLinks: 'throw',
|
||||||
onBrokenMarkdownLinks: 'warn',
|
onBrokenMarkdownLinks: 'warn',
|
||||||
|
|
|
@ -17724,6 +17724,11 @@ rsvp@^4.8.4:
|
||||||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
||||||
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
|
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
|
||||||
|
|
||||||
|
rtl-detect@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.2.tgz#8eca316f5c6563d54df4e406171dd7819adda67f"
|
||||||
|
integrity sha512-5X1422hvphzg2a/bo4tIDbjFjbJUOaPZwqE6dnyyxqwFqfR+tBcvfqapJr0o0VygATVCGKiODEewhZtKF+90AA==
|
||||||
|
|
||||||
rtlcss@^2.6.2:
|
rtlcss@^2.6.2:
|
||||||
version "2.6.2"
|
version "2.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-2.6.2.tgz#55b572b52c70015ba6e03d497e5c5cb8137104b4"
|
resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-2.6.2.tgz#55b572b52c70015ba6e03d497e5c5cb8137104b4"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue