From 6cc12e37a0477e2ae106b9ea145e7a8b405fd405 Mon Sep 17 00:00:00 2001 From: wade Date: Wed, 7 May 2025 09:46:47 +0800 Subject: [PATCH 1/8] v0.1.0 --- README.md | 4 ++-- package.json | 2 +- src/services/translation.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 58cbc14..c7b82a0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ You don't need to prepare any translation files, just provide your API key and t ## Features - 🌐 React Native and Expo support -- 🚀 Automatic string detection and translation +- 🚀 Automatic string translation - 🎯 Dynamic parameter interpolation - 🔍 Persist translation tracking - 🔌 Offline mode support @@ -125,7 +125,7 @@ Refer: https://docs.expo.dev/versions/latest/sdk/localization/ import * as Localization from "expo-localization"; // Get the device locale -const locale = const locale = Localization.getLocales()[0]?.languageCode; +const locale = Localization.getLocales()[0]?.languageCode; ``` Note: When running Expo in a web browser, it will use the browser's locale settings (navigator.language) automatically. diff --git a/package.json b/package.json index b5b8b16..ceaacf0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "react-native-autolocalise", - "version": "0.0.3-alpha.1", + "version": "0.1.0", "description": "Auto-translation SDK for React Native and Expo applications", "main": "dist/index.js", "module": "dist/index.esm.js", diff --git a/src/services/translation.ts b/src/services/translation.ts index 1f0d4a8..67c1365 100644 --- a/src/services/translation.ts +++ b/src/services/translation.ts @@ -84,7 +84,6 @@ export class TranslationService { this.pendingTranslations.forEach((persist, text) => { allTexts.push({ hashkey: this.generateHash(text), text, persist }); }); - console.log("allTexts", allTexts); this.pendingTranslations.clear(); if (allTexts.length > 0) { @@ -168,6 +167,7 @@ export class TranslationService { public translate( text: string, persist: boolean = true, + // eslint-disable-next-line @typescript-eslint/no-unused-vars reference?: string ): string { if (!text || !this.isInitialized) return text; From cd7c0836352cf12a3dce644205813d416621b8bb Mon Sep 17 00:00:00 2001 From: wade Date: Wed, 7 May 2025 10:05:24 +0800 Subject: [PATCH 2/8] update test branch triggering --- .github/workflows/test.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b2760a8..36b341b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,17 +1,9 @@ name: Test on: - pull_request: - branches: - - 'master' push: branches: - '*' - paths-ignore: - - 'README.md' - - 'LICENSE' - - 'CODE_OF_CONDUCT.md' - - 'CONTRIBUTING.md' jobs: test: From be20ef53991cf1811958505fa2b3c749d019c273 Mon Sep 17 00:00:00 2001 From: wade Date: Wed, 7 May 2025 10:16:39 +0800 Subject: [PATCH 3/8] add manual trigger test flow --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 36b341b..b79328c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,7 @@ on: push: branches: - '*' + workflow_dispatch: jobs: test: From 28fce17750c18eb87261b7bebbd9c28ab99e1cc0 Mon Sep 17 00:00:00 2001 From: wade Date: Sat, 17 May 2025 10:17:30 +0800 Subject: [PATCH 4/8] Nested formatting --- README.md | 28 +++++++++- package.json | 2 +- src/components/FormattedText.tsx | 94 ++++++++++++++++++++++++++++++++ src/index.ts | 1 + 4 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 src/components/FormattedText.tsx diff --git a/README.md b/README.md index c7b82a0..2f323a1 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ You don't need to prepare any translation files, just provide your API key and t - 🎯 Dynamic parameter interpolation - 🔍 Persist translation tracking - 🔌 Offline mode support +- 🎨 Nested text formatting support - ⚙️ Configurable cache TTL - ⚡️ Lightweight and efficient @@ -49,7 +50,7 @@ const App = () => { ### 2. Use the Translation Hook -Basic usage: +**Basic usage:** ```typescript import { useAutoTranslate } from "react-native-autolocalise"; @@ -61,12 +62,35 @@ const MyComponent = () => {

{t("Welcome to our app!", false)}

{t("This text will be automatically translated")}

+

+ welcome +

to

our app +

); }; ``` -Use with params: +**Use with nested text formatting:** + +```typescript +import { useAutoTranslate, FormattedText } from "react-native-autolocalise"; + +const MyComponent = () => { + const { t } = useAutoTranslate(); + + return ( + + + Hello, we want you to be{" "} + happy! + + + ); +}; +``` + +**Use with params:** ```typescript import { useAutoTranslate } from "react-native-autolocalise"; diff --git a/package.json b/package.json index ceaacf0..6241d9b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "react-native-autolocalise", - "version": "0.1.0", + "version": "0.2.0", "description": "Auto-translation SDK for React Native and Expo applications", "main": "dist/index.js", "module": "dist/index.esm.js", diff --git a/src/components/FormattedText.tsx b/src/components/FormattedText.tsx new file mode 100644 index 0000000..423260b --- /dev/null +++ b/src/components/FormattedText.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { Text, TextStyle } from "react-native"; +import { useAutoTranslate } from "../context/TranslationContext"; + +/** + * FormattedText is a component that handles nested text formatting during translation. + * It preserves styling and structure of nested Text components while allowing the content + * to be translated. + * + * @example + * ```tsx + * + * Hello, world! + * + * ``` + */ +interface FormattedTextProps { + children: React.ReactNode; + style?: TextStyle; +} + +export const FormattedText: React.FC = ({ + children, + style, +}) => { + const { t } = useAutoTranslate(); + + /** + * Extracts text content and styled nodes from the children prop. + * Converts nested Text components into a template format (e.g., <0>styled text) + * while preserving the original styled nodes for later restoration. + * + * @param nodes - The React nodes to process (typically the children prop) + * @returns An object containing the template text and an array of styled nodes + */ + const extractTextAndStyles = ( + nodes: React.ReactNode + ): { + text: string; + styles: Array<{ node: React.ReactElement; text: string }>; + } => { + const styles: Array<{ node: React.ReactElement; text: string }> = []; + let text = ""; + + const processNode = (node: React.ReactNode) => { + if (typeof node === "string") { + text += node; + return; + } + + if (React.isValidElement(node)) { + const children = node.props.children; + if (typeof children === "string") { + text += `<${styles.length}>${children}`; + styles.push({ node, text: children }); + } else if (Array.isArray(children)) { + children.forEach(processNode); + } + } + }; + + processNode(nodes); + return { text, styles }; + }; + + /** + * Restores the styled nodes in the translated text by replacing template markers + * with the original styled components, but with translated content. + * + * @param translatedText - The translated text containing template markers + * @param styles - Array of original styled nodes and their text content + * @returns An array of React nodes with restored styling and translated content + */ + const restoreStyledText = ( + translatedText: string, + styles: Array<{ node: React.ReactElement; text: string }> + ): React.ReactNode[] => { + const parts = translatedText.split(/(<\d+>.*?<\/\d+>)/g); + return parts.map((part, index) => { + const match = part.match(/<(\d+)>(.*?)<\/\1>/); + if (match) { + const [, styleIndex, content] = match; + const { node } = styles[parseInt(styleIndex)]; + return React.cloneElement(node, { key: `styled-${index}` }, content); + } + return part; + }); + }; + + const { text, styles } = extractTextAndStyles(children); + const translatedText = t(text); + + return {restoreStyledText(translatedText, styles)}; +}; diff --git a/src/index.ts b/src/index.ts index 4cd7e2f..0054342 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,5 +13,6 @@ const autoTranslate = { return service.init(); }, }; +export { FormattedText } from "./components/FormattedText"; export default autoTranslate; From be203aefc6f01cfb58b4cc20d336493768541fcc Mon Sep 17 00:00:00 2001 From: wade Date: Sat, 17 May 2025 10:27:25 +0800 Subject: [PATCH 5/8] handle persist flag --- README.md | 41 ++++++++++++++++++-------------- src/components/FormattedText.tsx | 8 ++++++- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 2f323a1..2fb55c1 100644 --- a/README.md +++ b/README.md @@ -53,20 +53,17 @@ const App = () => { **Basic usage:** ```typescript +import { View, Text } from "react-native"; import { useAutoTranslate } from "react-native-autolocalise"; const MyComponent = () => { const { t, loading, error } = useAutoTranslate(); return ( -
-

{t("Welcome to our app!", false)}

-

{t("This text will be automatically translated")}

-

- welcome -

to

our app -

-
+ + {t("Welcome to our app!", false)} + {t("This text will be automatically translated")} + ); }; ``` @@ -74,18 +71,25 @@ const MyComponent = () => { **Use with nested text formatting:** ```typescript +import { Text, View } from "react-native"; import { useAutoTranslate, FormattedText } from "react-native-autolocalise"; const MyComponent = () => { const { t } = useAutoTranslate(); return ( - - - Hello, we want you to be{" "} - happy! - - + + + + Hello, we want you to be{" "} + happy! + + + + Hello, + World + + ); }; ``` @@ -93,6 +97,7 @@ const MyComponent = () => { **Use with params:** ```typescript +import { View, Text } from "react-native"; import { useAutoTranslate } from "react-native-autolocalise"; const MyComponent = () => { @@ -100,13 +105,13 @@ const MyComponent = () => { const name = "John"; return ( -
-

+ + {t("Welcome, {{1}}!, Nice to meet you. {{2}}.") .replace("{{1}}", name) .replace("{{2}}", t("Have a great day!"))} -

-
+ + ); }; ``` diff --git a/src/components/FormattedText.tsx b/src/components/FormattedText.tsx index 423260b..f8a1794 100644 --- a/src/components/FormattedText.tsx +++ b/src/components/FormattedText.tsx @@ -17,11 +17,17 @@ import { useAutoTranslate } from "../context/TranslationContext"; interface FormattedTextProps { children: React.ReactNode; style?: TextStyle; + /** + * Whether to persist the text for review in the dashboard. + * @default true + */ + persist?: boolean; } export const FormattedText: React.FC = ({ children, style, + persist = true, }) => { const { t } = useAutoTranslate(); @@ -88,7 +94,7 @@ export const FormattedText: React.FC = ({ }; const { text, styles } = extractTextAndStyles(children); - const translatedText = t(text); + const translatedText = t(text, persist); return {restoreStyledText(translatedText, styles)}; }; From 75d33e833693b99a54e3a8d930ec3da4fbf23555 Mon Sep 17 00:00:00 2001 From: wade Date: Sun, 18 May 2025 14:39:11 +0800 Subject: [PATCH 6/8] update readme --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 2fb55c1..918b2a9 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,9 @@ const MyComponent = () => { ```typescript import { Text, View } from "react-native"; -import { useAutoTranslate, FormattedText } from "react-native-autolocalise"; +import { FormattedText } from "react-native-autolocalise"; const MyComponent = () => { - const { t } = useAutoTranslate(); - return ( From c49e137cf602327a30f062102648b3038e0c8479 Mon Sep 17 00:00:00 2001 From: wade Date: Thu, 7 Aug 2025 14:38:06 +0800 Subject: [PATCH 7/8] add version --- package.json | 7 ++++--- scripts/update-version.js | 18 ++++++++++++++++++ src/services/translation.ts | 4 +++- src/types.ts | 1 + src/version.ts | 3 +++ 5 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 scripts/update-version.js create mode 100644 src/version.ts diff --git a/package.json b/package.json index 6241d9b..e9ad77f 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "types": "dist/index.d.ts", "sideEffects": false, "scripts": { + "prebuild": "node scripts/update-version.js", "build": "rollup -c", "test": "jest", "test:watch": "jest --watch", @@ -24,9 +25,9 @@ "author": "AutoLocalise", "license": "MIT", "peerDependencies": { - "react": ">=18.0.0", - "react-native": ">=0.73.0", - "@react-native-async-storage/async-storage": ">=1.18.0" + "react": ">=16.8.0", + "react-native": ">=0.60.0", + "@react-native-async-storage/async-storage": ">=1.0.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.0", diff --git a/scripts/update-version.js b/scripts/update-version.js new file mode 100644 index 0000000..615b4c4 --- /dev/null +++ b/scripts/update-version.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +// Read package.json +const packageJsonPath = path.join(__dirname, "..", "package.json"); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + +// Update version.ts +const versionFilePath = path.join(__dirname, "..", "src", "version.ts"); +const versionContent = `// This file is auto-generated during build +// Update this manually or via build script +export const VERSION = "${packageJson.version}"; +`; + +fs.writeFileSync(versionFilePath, versionContent); +console.log(`Updated version.ts with version ${packageJson.version}`); diff --git a/src/services/translation.ts b/src/services/translation.ts index 67c1365..9b2e32f 100644 --- a/src/services/translation.ts +++ b/src/services/translation.ts @@ -6,6 +6,7 @@ import { TranslationResponse, } from "../types"; import { getStorageAdapter } from "../storage"; +import { VERSION } from "../version"; export class TranslationService { private config: TranslationConfig; @@ -87,12 +88,13 @@ export class TranslationService { this.pendingTranslations.clear(); if (allTexts.length > 0) { - // Modify API request to include context + // Modify API request to include context and version const request: TranslationRequest = { texts: allTexts, sourceLocale: this.config.sourceLocale, targetLocale: this.config.targetLocale, apiKey: this.config.apiKey, + version: `react-native-v${VERSION}`, }; try { diff --git a/src/types.ts b/src/types.ts index a914225..844ec78 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,7 @@ export interface TranslationRequest { sourceLocale: string; targetLocale: string; apiKey: string; + version: string; } export interface TranslationResponse { diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..e6cabdb --- /dev/null +++ b/src/version.ts @@ -0,0 +1,3 @@ +// This file is auto-generated during build +// Update this manually or via build script +export const VERSION = "0.0.0"; From 4fcc623a127fa2a694b811a182b5d92b8a09647f Mon Sep 17 00:00:00 2001 From: wade Date: Thu, 7 Aug 2025 14:57:12 +0800 Subject: [PATCH 8/8] update type error --- package.json | 3 +-- scripts/update-version.js | 18 ------------------ src/storage/index.ts | 13 ++++++------- src/types/async-storage.d.ts | 10 ++++++++++ src/version.ts | 2 +- 5 files changed, 18 insertions(+), 28 deletions(-) delete mode 100644 scripts/update-version.js create mode 100644 src/types/async-storage.d.ts diff --git a/package.json b/package.json index e9ad77f..c0c1f35 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,13 @@ { "type": "module", "name": "react-native-autolocalise", - "version": "0.2.0", + "version": "0.2.1", "description": "Auto-translation SDK for React Native and Expo applications", "main": "dist/index.js", "module": "dist/index.esm.js", "types": "dist/index.d.ts", "sideEffects": false, "scripts": { - "prebuild": "node scripts/update-version.js", "build": "rollup -c", "test": "jest", "test:watch": "jest --watch", diff --git a/scripts/update-version.js b/scripts/update-version.js deleted file mode 100644 index 615b4c4..0000000 --- a/scripts/update-version.js +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env node - -const fs = require("fs"); -const path = require("path"); - -// Read package.json -const packageJsonPath = path.join(__dirname, "..", "package.json"); -const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); - -// Update version.ts -const versionFilePath = path.join(__dirname, "..", "src", "version.ts"); -const versionContent = `// This file is auto-generated during build -// Update this manually or via build script -export const VERSION = "${packageJson.version}"; -`; - -fs.writeFileSync(versionFilePath, versionContent); -console.log(`Updated version.ts with version ${packageJson.version}`); diff --git a/src/storage/index.ts b/src/storage/index.ts index 6237a6a..1b53fac 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -1,10 +1,4 @@ -// import AsyncStorage from "@react-native-async-storage/async-storage"; - -interface StorageAdapter { - getItem: (key: string) => Promise; - setItem: (key: string, value: string) => Promise; - removeItem: (key: string) => Promise; -} +import { StorageAdapter } from "../types"; export async function getStorageAdapter(): Promise { try { @@ -14,6 +8,11 @@ export async function getStorageAdapter(): Promise { if (AsyncStorage?.default) { return AsyncStorage.default; } + + // If AsyncStorage is not available, throw an error + throw new Error( + "No storage adapter available. Please install @react-native-async-storage/async-storage" + ); } catch (e) { throw new Error( "No storage adapter available. Please install @react-native-async-storage/async-storage" diff --git a/src/types/async-storage.d.ts b/src/types/async-storage.d.ts new file mode 100644 index 0000000..983f506 --- /dev/null +++ b/src/types/async-storage.d.ts @@ -0,0 +1,10 @@ +declare module "@react-native-async-storage/async-storage" { + interface AsyncStorageStatic { + getItem(key: string): Promise; + setItem(key: string, value: string): Promise; + removeItem(key: string): Promise; + } + + const AsyncStorage: AsyncStorageStatic; + export default AsyncStorage; +} diff --git a/src/version.ts b/src/version.ts index e6cabdb..74857b6 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ // This file is auto-generated during build // Update this manually or via build script -export const VERSION = "0.0.0"; +export const VERSION = "0.2.1";