Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 多言語化対応の基盤を作成 #5220

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions packages/smarthr-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dependencies": {
"@smarthr/wareki": "^1.3.0",
"dayjs": "^1.11.13",
"intl-messageformat": "^10.7.10",
"lodash.merge": "^4.6.2",
"lodash.range": "^3.2.0",
"polished": "^4.3.0",
Expand Down Expand Up @@ -74,6 +75,7 @@
"react-docgen-typescript": "^2.2.2",
"react-dom": "^19.0.0",
"react-ga4": "^2.1.0",
"react-intl": "^7.0.4",
"react-test-renderer": "^19.0.0",
"rimraf": "^6.0.1",
"standard-version": "^9.3.2",
Expand All @@ -91,6 +93,7 @@
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"react-intl": "^7.0.4",
"styled-components": "^5.0.1"
},
"bugs": {
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { ComponentProps } from 'react'
import { FormattedMessage as RawFormattedMessage } from 'react-intl'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { FormattedMessage as RawFormattedMessage } from 'react-intl'
import { FormattedMessage as ReactIntlFormattedMessage } from 'react-intl'


import { locale as ja } from '../../locales/ja'
import { Messages } from '../../locales/types'

type Props<Id extends keyof Messages> = Omit<
ComponentProps<typeof RawFormattedMessage>,
'id' | 'defaultMessage'
> & {
id: Id
defaultMessage: (typeof ja)[Id]
}

export const FormattedMessage = <ID extends keyof Messages>({ values, ...props }: Props<ID>) => (
<RawFormattedMessage {...props} values={{ break: <br />, ...values }} />
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client'

import React, { FC, PropsWithChildren } from 'react'
import { IntlProvider as ReactIntlProvider } from 'react-intl'

import { locale as ja } from '../../locales/ja'
import { Messages } from '../../locales/types'

const localeMap = {
ja,
} as const

type Props = PropsWithChildren<{
locale: keyof typeof localeMap
messages: Messages
}>

export const IntlProvider: FC<Props> = ({ locale, children }) => (
<ReactIntlProvider locale={locale} messages={localeMap[locale]}>
{children}
</ReactIntlProvider>
)
10 changes: 10 additions & 0 deletions packages/smarthr-ui/src/components/IntlProvider/localeMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const localeMap = {
ja: '日本語',
'id-id': 'Bahasa Indonesia',
'en-us': 'English',
pt: 'Português',
vi: 'Tiếng Việt',
ko: '한국어',
'zh-cn': '简体中文',
'zh-tw': '繁體中文',
} as const
13 changes: 11 additions & 2 deletions packages/smarthr-ui/src/components/TextLink/TextLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React, {
} from 'react'
import { tv } from 'tailwind-variants'

import { useIntl } from '../../hooks/useIntl'
import { ElementRef, ElementRefProps } from '../../types'
import { FaUpRightFromSquareIcon } from '../Icon'

Expand Down Expand Up @@ -61,13 +62,21 @@ export const TextLink: TextLinkComponent = forwardRef(
}: PropsWithoutRef<Props<T>> & ElementProps<T>,
ref: Ref<ElementRef<T>>,
) => {
const { formatMessage } = useIntl()
const Component = elementAs || 'a'
const actualSuffix = useMemo(() => {
if (target === '_blank' && suffix === undefined) {
return <FaUpRightFromSquareIcon aria-label="別タブで開く" />
return (
<FaUpRightFromSquareIcon
aria-label={formatMessage({
id: 'smarthr-ui/TextLink/OpenInNewTab',
defaultMessage: '別タブで開く',
})}
/>
)
}
return suffix
}, [suffix, target])
}, [suffix, target, formatMessage])
const actualHref = useMemo(() => {
if (href) {
return href
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { action } from '@storybook/addon-actions'
import React, { ComponentPropsWithoutRef } from 'react'

import { FaCircleQuestionIcon, FaUpRightFromSquareIcon } from '../../Icon'
import { IntlProvider } from '../../IntlProvider/IntlProvider'
import { UpwardLink } from '../../UpwardLink'
import { TextLink } from '../TextLink'

Expand Down Expand Up @@ -30,7 +31,11 @@ export default {
title: 'Navigation(ナビゲーション)/TextLink',
component: TextLink,
subcomponents: { UpwardLink },
render: (args) => <TextLink {...args} />,
render: (args) => (
<IntlProvider locale="ja">
<TextLink {...args} />
</IntlProvider>
),
argTypes: {
href: {
control: 'text',
Expand Down
54 changes: 54 additions & 0 deletions packages/smarthr-ui/src/hooks/useIntl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { FormatXMLElementFn, Options as IntlMessageFormatOptions } from 'intl-messageformat'
import { useCallback } from 'react'
import {
PrimitiveType,
MessageDescriptor as ReactIntlMessageDescriptor,
useIntl as useReactIntl,
} from 'react-intl'

import { locale as ja } from '../locales/ja'
import { Messages } from '../locales/types'

/**
* MEMO:
* src/components/parts/FormattedMessage/FormattedMessage.tsx
* と同じやり方で型を拡張している
*/
type MessageDescriptor<T extends keyof Messages> = Omit<ReactIntlMessageDescriptor, 'id'> & {
id: T
defaultMessage: (typeof ja)[T]
}

export const useIntl = () => {
const intl = useReactIntl()
// const lang = useLanguage()
const lang = intl.locale

const formatMessage = useCallback(
<T extends keyof Messages>(
descriptor: MessageDescriptor<T>,
values?: Record<string, PrimitiveType | FormatXMLElementFn<string, string>>,
opts?: IntlMessageFormatOptions,
): string => intl.formatMessage(descriptor, values, opts),
[intl],
)

const formatDate = useCallback(
(date: Date, opts?: Intl.DateTimeFormatOptions & { jaFormat?: boolean }): string => {
// 日本語の場合デザインシステム上のフォーマットは「YYYY/MM/DD」形式なので、それに合わせるための対応
const slashFormat = !opts?.jaFormat && lang === 'ja'

const overrideOpts: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: slashFormat ? '2-digit' : 'short',
day: '2-digit',
...opts,
} as const

return intl.formatDate(date, overrideOpts)
},
[intl, lang],
)

return { formatMessage, formatDate }
}
5 changes: 5 additions & 0 deletions packages/smarthr-ui/src/locales/en_us.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Messages } from './types'

export const locale = {
'smarthr-ui/TextLink/OpenInNewTab': '',
} as const satisfies Messages
5 changes: 5 additions & 0 deletions packages/smarthr-ui/src/locales/ja.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Messages } from './types'

export const locale = {
'smarthr-ui/TextLink/OpenInNewTab': '別タブで開く',
} as const satisfies Messages
3 changes: 3 additions & 0 deletions packages/smarthr-ui/src/locales/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type Messages = {
'smarthr-ui/TextLink/OpenInNewTab': string
}
98 changes: 98 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading