diff --git a/README.md b/README.md index 9eb4445..6ba5552 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,35 @@ each node. ; ``` +## Internationalization + +You can render translated strings by passing a language code and a translations map. When a translation for a given key is missing, the component falls back to the original string. + +```jsx +import TextLoop from "react-text-loop"; + +const translations = { + en: { + trade: "Trade faster", + increase: "Increase sales", + }, + zh: { + trade: "交易更快", + increase: "提高销售", + }, + es: { + trade: "Comercia más rápido", + increase: "Aumenta ventas", + }, +}; + +// Using string keys so translations can be applied +; + +// If a translation key is missing, TextLoop will fall back to the original value +; +``` + ## Examples ### Fast transition diff --git a/src/__tests__/TextLoop.test.tsx b/src/__tests__/TextLoop.test.tsx new file mode 100644 index 0000000..09436c7 --- /dev/null +++ b/src/__tests__/TextLoop.test.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { render, cleanup } from "@testing-library/react"; +import TextLoop from "../components/TextLoop"; + +afterEach(() => { + cleanup(); +}); + +describe("TextLoop Internationalization", () => { + it("(a) default language behavior remains unchanged", () => { + const { getByText } = render(); + // Should render the first item unchanged + getByText("First"); + }); + + it("(b) switches language and updates rendered text", () => { + const translations = { + zh: { greeting: "你好", farewell: "再见" }, + es: { greeting: "Hola", farewell: "Adiós" }, + }; + + const { rerender, getByText } = render( + + ); + + // Should display the Chinese translation for the first key + getByText("你好"); + + // Switch to Spanish and ensure text updates + rerender( + + ); + getByText("Hola"); + }); + + it("(c) falls back to original value when translation key is missing", () => { + const translations = { zh: { known: "已知" } }; + const { getByText } = render( + + ); + + // First item has no translation; should fall back to the original value + getByText("unknown"); + }); +}); diff --git a/src/components/TextLoop.tsx b/src/components/TextLoop.tsx index 51866ba..af527a8 100644 --- a/src/components/TextLoop.tsx +++ b/src/components/TextLoop.tsx @@ -23,6 +23,9 @@ type Props = { noWrap: boolean; className?: string; onChange?: Function; + // i18n additions + lang?: string; + translations?: Record>; }; type State = { @@ -54,7 +57,15 @@ class TextLoop extends React.PureComponent { constructor(props: Props) { super(props); - const elements = React.Children.toArray(props.children); + const baseElements = React.Children.toArray(props.children) as ( + JSX.Element | string + )[]; + + const elements = this.applyTranslations( + baseElements, + props.lang, + props.translations + ); this.state = { elements, @@ -109,10 +120,19 @@ class TextLoop extends React.PureComponent { } } - if (!isEqual(prevProps.children, children)) { + // Update elements when children, lang, or translations change + if ( + !isEqual(prevProps.children, children) || + prevProps.lang !== this.props.lang || + !isEqual(prevProps.translations, this.props.translations) + ) { // eslint-disable-next-line react/no-did-update-set-state this.setState({ - elements: React.Children.toArray(children), + elements: this.applyTranslations( + React.Children.toArray(children) as (JSX.Element | string)[], + this.props.lang, + this.props.translations + ), }); } } @@ -132,6 +152,25 @@ class TextLoop extends React.PureComponent { } } + // Map strings through translations when available; keep JSX elements intact + applyTranslations( + elements: (JSX.Element | string | undefined)[], + lang?: string, + translations?: Record> + ): (JSX.Element | string | undefined)[] { + if (!lang || !translations) { + return elements; + } + const pack = translations[lang] || {}; + return elements.map(el => { + if (typeof el === "string") { + const translated = pack[el]; + return translated != null ? translated : el; + } + return el; + }); + } + // Fade out animation willLeave = (): { opacity: OpaqueConfig; translate: OpaqueConfig } => { const { height } = this.getDimensions(); diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..b9103ef --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,19 @@ +import { LanguageCode } from "./types"; + +// Predefined (empty) language packs. Library users can enrich these as needed. +export const DEFAULT_TRANSLATIONS: Record> = { + en: {}, + zh: {}, + es: {}, +}; + +// Lightweight translation function: returns the translated string if available, +// otherwise falls back to the provided key. +export function t(key: string, lang: string): string { + const code = lang as LanguageCode; + const pack = DEFAULT_TRANSLATIONS[code]; + if (pack && Object.prototype.hasOwnProperty.call(pack, key)) { + return pack[key]; + } + return key; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..b27aa1b --- /dev/null +++ b/src/types.ts @@ -0,0 +1,3 @@ +export type LanguageCode = "en" | "zh" | "es"; + +export type Translations = Record>;