Skip to content
Open
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,35 @@ each node.
<TextLoop children={["Trade faster", "Increase sales", "Stock winners", "Price perfectly"]} />;
```

## 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
<TextLoop lang="zh" translations={translations} children={["trade", "increase"]} />;

// If a translation key is missing, TextLoop will fall back to the original value
<TextLoop lang="es" translations={translations} children={["unknown", "trade"]} />;
```

## Examples

### Fast transition
Expand Down
45 changes: 45 additions & 0 deletions src/__tests__/TextLoop.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<TextLoop children={["First", "Second"]} />);
// 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(
<TextLoop lang="zh" translations={translations} children={["greeting", "farewell"]} />
);

// Should display the Chinese translation for the first key
getByText("你好");

// Switch to Spanish and ensure text updates
rerender(
<TextLoop lang="es" translations={translations} children={["greeting", "farewell"]} />
);
getByText("Hola");
});

it("(c) falls back to original value when translation key is missing", () => {
const translations = { zh: { known: "已知" } };
const { getByText } = render(
<TextLoop lang="zh" translations={translations} children={["unknown", "known"]} />
);

// First item has no translation; should fall back to the original value
getByText("unknown");
});
});
45 changes: 42 additions & 3 deletions src/components/TextLoop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ type Props = {
noWrap: boolean;
className?: string;
onChange?: Function;
// i18n additions
lang?: string;
translations?: Record<string, Record<string, string>>;
};

type State = {
Expand Down Expand Up @@ -54,7 +57,15 @@ class TextLoop extends React.PureComponent<Props, State> {

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,
Expand Down Expand Up @@ -109,10 +120,19 @@ class TextLoop extends React.PureComponent<Props, State> {
}
}

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
),
});
}
}
Expand All @@ -132,6 +152,25 @@ class TextLoop extends React.PureComponent<Props, State> {
}
}

// Map strings through translations when available; keep JSX elements intact
applyTranslations(
elements: (JSX.Element | string | undefined)[],
lang?: string,
translations?: Record<string, Record<string, string>>
): (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();
Expand Down
19 changes: 19 additions & 0 deletions src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { LanguageCode } from "./types";

// Predefined (empty) language packs. Library users can enrich these as needed.
export const DEFAULT_TRANSLATIONS: Record<LanguageCode, Record<string, string>> = {
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;
}
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type LanguageCode = "en" | "zh" | "es";

export type Translations = Record<string, Record<string, string>>;