Implemented project-wide localization plumbing and converted the visible UI/app messages to translation-ready strings.
This commit is contained in:
103
resources/js/lib/translations.tsx
Normal file
103
resources/js/lib/translations.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import type { TranslationReplacements, Translations } from '@/types';
|
||||
|
||||
export type Translate = (
|
||||
key: string,
|
||||
replacements?: TranslationReplacements,
|
||||
) => string;
|
||||
|
||||
export function translateString(
|
||||
translations: Translations,
|
||||
key: string,
|
||||
replacements: TranslationReplacements = {},
|
||||
): string {
|
||||
return Object.entries(replacements).reduce(
|
||||
(message, [name, value]) =>
|
||||
message
|
||||
.replaceAll(`:${name}`, String(value ?? ''))
|
||||
.replaceAll(`{${name}}`, String(value ?? '')),
|
||||
translations[key] ?? key,
|
||||
);
|
||||
}
|
||||
|
||||
export function translateChildren(
|
||||
children: ReactNode,
|
||||
translate: Translate,
|
||||
): ReactNode {
|
||||
return Children.map(children, (child) => {
|
||||
if (typeof child === 'string') {
|
||||
return translateTextNode(child, translate);
|
||||
}
|
||||
|
||||
if (!isValidElement(child)) {
|
||||
return child;
|
||||
}
|
||||
|
||||
const element = child as ReactElement<{ children?: ReactNode }>;
|
||||
|
||||
if (!element.props.children) {
|
||||
return element;
|
||||
}
|
||||
|
||||
return cloneElement(
|
||||
element,
|
||||
undefined,
|
||||
translateChildren(element.props.children, translate),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useTranslation(): {
|
||||
locale: string;
|
||||
t: Translate;
|
||||
translateNode: (children: ReactNode) => ReactNode;
|
||||
} {
|
||||
const { locale, translations } = usePage().props;
|
||||
|
||||
const t = useCallback<Translate>(
|
||||
(key, replacements = {}) =>
|
||||
translateString(translations, key, replacements),
|
||||
[translations],
|
||||
);
|
||||
|
||||
const translateNode = useCallback(
|
||||
(children: ReactNode) => translateChildren(children, t),
|
||||
[t],
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
locale,
|
||||
t,
|
||||
translateNode,
|
||||
}),
|
||||
[locale, t, translateNode],
|
||||
);
|
||||
}
|
||||
|
||||
export function useTranslatedChildren(children: ReactNode): ReactNode {
|
||||
const { translateNode } = useTranslation();
|
||||
|
||||
return translateNode(children);
|
||||
}
|
||||
|
||||
function translateTextNode(value: string, translate: Translate): string {
|
||||
const text = value.trim();
|
||||
|
||||
if (text === '' || !/[A-Za-z]/.test(text)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const leadingWhitespace = value.match(/^\s*/)?.[0] ?? '';
|
||||
const trailingWhitespace = value.match(/\s*$/)?.[0] ?? '';
|
||||
|
||||
return `${leadingWhitespace}${translate(text)}${trailingWhitespace}`;
|
||||
}
|
||||
Reference in New Issue
Block a user