Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion apps/site/components/Downloads/Release/ReleaseCodeBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ReleaseContext,
ReleasesContext,
} from '#site/providers/releaseProvider';
import type { IntlMessageKeys } from '#site/types/i18n.js';
import type { ReleaseContextType } from '#site/types/release';
import { INSTALL_METHODS } from '#site/util/downloadUtils';

Expand Down Expand Up @@ -144,7 +145,7 @@ const ReleaseCodeBox: FC = () => {

<span className="text-center text-xs text-neutral-800 dark:text-neutral-200">
<Skeleton loading={renderSkeleton} hide={!currentPlatform}>
{t(info, { platform: label })}{' '}
{t(info as IntlMessageKeys, { platform: label })}{' '}
{t.rich('layouts.download.codeBox.externalSupportInfo', {
platform: label,
link: text => (
Expand Down
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
10 changes: 5 additions & 5 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 @@ -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,7 +112,7 @@ 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,
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 All @@ -143,7 +143,7 @@ export const PLATFORMS = Object.fromEntries(
Object.entries(systems).map(([key, data]) => [
key,
data.platforms.map(platform => ({
label: platform.label,
label: platform.label as IntlMessageKeys,
value: platform.value as UserPlatform,
compatibility: platform.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