Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 5 additions & 27 deletions apps/site/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,7 @@
import type baseMessages from '@node-core/website-i18n/locales/en.json';
import type { MessageKeys, NestedValueOf, NestedKeyOf } from 'next-intl';
import { Locale } from '@node-core/website-i18n/types';

declare global {
// Defines a type for all the IntlMessage shape (which is used internall by next-intl)
// @see https://next-intl.dev/docs/workflows/typescript
type IntlMessages = typeof baseMessages;

// Defines a generic type for all available i18n translation keys, by default not using any namespace
type IntlMessageKeys<
NestedKey extends NamespaceKeys<
IntlMessages,
NestedKeyOf<IntlMessages>
> = never,
> = MessageKeys<
NestedValueOf<
{ '!': IntlMessages },
[NestedKey] extends [never] ? '!' : `!.${NestedKey}`
>,
NestedKeyOf<
NestedValueOf<
{ '!': IntlMessages },
[NestedKey] extends [never] ? '!' : `!.${NestedKey}`
>
>
>;
declare module 'next-intl' {
interface AppConfig {
Messages: Locale;
}
}

export {};
1 change: 1 addition & 0 deletions apps/site/hooks/react-generic/useSiteNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { HTMLAttributeAnchorTarget } from 'react';
import { siteNavigation } from '#site/next.json.mjs';
import type {
FormattedMessage,
IntlMessageKeys,
NavigationEntry,
NavigationKeys,
} from '#site/types';
Expand Down
6 changes: 2 additions & 4 deletions apps/site/tests/e2e/general-behavior.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { importLocale } from '@node-core/website-i18n';
import type { Locale } from '@node-core/website-i18n/types';
import { test, expect, type Page } from '@playwright/test';

const englishLocale = await importLocale('en');
Expand Down Expand Up @@ -34,10 +35,7 @@ const openLanguageMenu = async (page: Page) => {
return page.locator(selector);
};

const verifyTranslation = async (
page: Page,
locale: string | Record<string, unknown>
) => {
const verifyTranslation = async (page: Page, locale: Locale | string) => {
// Load locale data if string code provided (e.g., 'es', 'fr')
const localeData =
typeof locale === 'string' ? await importLocale(locale) : locale;
Expand Down
2 changes: 2 additions & 0 deletions apps/site/types/blog.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { IntlMessageKeys } from './i18n';

export type BlogPreviewType = 'announcements' | 'release' | 'vulnerability';
export type BlogCategory = IntlMessageKeys<'layouts.blog.categories'>;

Expand Down
23 changes: 23 additions & 0 deletions apps/site/types/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
import type { Locale } from '@node-core/website-i18n/types';
import type {
NamespaceKeys,
MessageKeys,
NestedValueOf,
NestedKeyOf,
} from 'next-intl';
import type { JSXElementConstructor, ReactElement, ReactNode } from 'react';

export type FormattedMessage =
| string
| ReactElement<HTMLElement, string | JSXElementConstructor<HTMLElement>>
| ReadonlyArray<ReactNode>;

// Defines a generic type for all available i18n translation keys, by default not using any namespace
export type IntlMessageKeys<
NestedKey extends NamespaceKeys<Locale, NestedKeyOf<Locale>> = never,
> = MessageKeys<
NestedValueOf<
{ '!': Locale },
[NestedKey] extends [never] ? '!' : `!.${NestedKey}`
>,
NestedKeyOf<
NestedValueOf<
{ '!': Locale },
[NestedKey] extends [never] ? '!' : `!.${NestedKey}`
>
>
>;
2 changes: 2 additions & 0 deletions apps/site/types/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { HTMLAttributeAnchorTarget } from 'react';

import type { IntlMessageKeys } from './i18n';

export interface FooterConfig {
text: IntlMessageKeys;
link: string;
Expand Down
14 changes: 7 additions & 7 deletions apps/site/util/downloadUtils/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as PackageManagerIcons from '@node-core/ui-components/Icons/PackageMana
import type { ElementType } from 'react';
import satisfies from 'semver/functions/satisfies';

import type { NodeReleaseStatus } from '#site/types';
import type { IntlMessageKeys, NodeReleaseStatus } from '#site/types';
import type * as Types from '#site/types/release';
import type { UserOS, UserPlatform } from '#site/types/userOS';

Expand Down Expand Up @@ -33,10 +33,10 @@ type DownloadCompatibility = {
};

type DownloadDropdownItem<T extends string> = {
label: string;
label: IntlMessageKeys;
recommended?: boolean;
url?: string;
info?: string;
info?: IntlMessageKeys;
compatibility: DownloadCompatibility;
} & Omit<SelectValue<T>, 'label'>;

Expand Down Expand Up @@ -102,7 +102,7 @@ type ActualSystems = Omit<typeof systems, 'OTHER' | 'LOADING'>;
export const OPERATING_SYSTEMS = Object.entries(systems as ActualSystems)
.filter(([key]) => key !== 'LOADING' && key !== 'OTHER')
.map(([key, data]) => ({
label: data.name,
label: data.name as IntlMessageKeys,
value: key as UserOS,
compatibility: data.compatibility,
iconImage: createIcon(OSIcons, data.icon),
Expand All @@ -112,11 +112,11 @@ export const OPERATING_SYSTEMS = Object.entries(systems as ActualSystems)
export const INSTALL_METHODS = installMethods.map(method => ({
key: method.id,
value: method.id as Types.InstallationMethod,
label: method.name,
label: method.name as IntlMessageKeys,
iconImage: createIcon(InstallMethodIcons, method.icon),
recommended: method.recommended,
url: method.url,
info: method.info,
info: method.info as IntlMessageKeys,
compatibility: {
...method.compatibility,
os: method.compatibility?.os?.map(os => os as UserOS),
Expand All @@ -130,7 +130,7 @@ export const INSTALL_METHODS = installMethods.map(method => ({
export const PACKAGE_MANAGERS = packageManagers.map(manager => ({
key: manager.id,
value: manager.id as Types.PackageManager,
label: manager.name,
label: manager.name as IntlMessageKeys,
iconImage: createIcon(PackageManagerIcons, manager.id),
compatibility: {
...manager.compatibility,
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/lib/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import localeConfig from '../config.json' with { type: 'json' };
* Imports a locale when exists from the locales directory
*
* @param {string} locale The locale code to import
* @returns {Promise<Record<string, any>>} The imported locale
* @returns {Promise<import('../types').Locale>} The imported locale
*/
export const importLocale = async locale => {
return import(`../locales/${locale}.json`, { with: { type: 'json' } }).then(
Expand Down
4 changes: 4 additions & 0 deletions packages/i18n/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type EnglishMessages from './locales/en.json';

export interface LocaleConfig {
code: string;
localName: string;
Expand All @@ -8,3 +10,5 @@ export interface LocaleConfig {
enabled: boolean;
default: boolean;
}

export type Locale = typeof EnglishMessages;
Loading