gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: i18n


From: gnunet
Subject: [taler-wallet-core] branch master updated: i18n
Date: Mon, 26 Jul 2021 16:34:01 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 44551245 i18n
44551245 is described below

commit 44551245dab36570d937affdb89735c937b4ae55
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Jul 26 11:27:56 2021 -0300

    i18n
---
 packages/taler-util/src/i18n.ts                    |  6 +-
 .../.storybook/preview.js                          | 20 ++-----
 .../taler-wallet-webextension/clean_and_build.sh   |  1 +
 .../src/components/SelectList.tsx                  | 40 +++++++++++++
 .../context/{useDevContext.ts => devContext.ts}    |  0
 .../src/context/translation.ts                     | 68 +++++++++++++++++++++
 .../taler-wallet-webextension/src/hooks/useLang.ts |  7 +++
 .../src/hooks/useLocalStorage.ts                   | 21 +++++++
 .../taler-wallet-webextension/src/i18n/strings.ts  | 70 ++++++++++++++++++++++
 .../src/popup/ProviderDetailPage.tsx               | 34 +++++------
 .../src/popup/Settings.tsx                         | 42 +++++++++++--
 .../taler-wallet-webextension/src/popup/popup.tsx  |  2 +-
 .../src/popupEntryPoint.tsx                        |  2 +-
 13 files changed, 272 insertions(+), 41 deletions(-)

diff --git a/packages/taler-util/src/i18n.ts b/packages/taler-util/src/i18n.ts
index 0324d8e0..e452ffa9 100644
--- a/packages/taler-util/src/i18n.ts
+++ b/packages/taler-util/src/i18n.ts
@@ -45,7 +45,7 @@ function toI18nString(stringSeq: ReadonlyArray<string>): 
string {
 /**
  * Internationalize a string template with arbitrary serialized values.
  */
-export function str(stringSeq: TemplateStringsArray, ...values: any[]): string 
{
+export function singular(stringSeq: TemplateStringsArray, ...values: any[]): 
string {
   const s = toI18nString(stringSeq);
   const tr = jed
     .translate(s)
@@ -141,7 +141,9 @@ function stringifyArray(children: Array<any>): string {
 }
 
 export const i18n = {
-  str,
+  str: singular,
+  singular,
   Translate,
   translate,
 };
+
diff --git a/packages/taler-wallet-webextension/.storybook/preview.js 
b/packages/taler-wallet-webextension/.storybook/preview.js
index af768dde..169b726f 100644
--- a/packages/taler-wallet-webextension/.storybook/preview.js
+++ b/packages/taler-wallet-webextension/.storybook/preview.js
@@ -14,15 +14,9 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { setupI18n } from "@gnu-taler/taler-util"
 import { Fragment } from "preact"
-import { strings } from '../src/i18n/strings.ts'
 import { NavBar } from '../src/popup/popup'
-
-const mockConfig = {
-  backendURL: 'http://demo.taler.net',
-  currency: 'KUDOS'
-}
+import { TranslationProvider } from '../src/context/translation'
 
 export const parameters = {
   controls: { expanded: true },
@@ -38,7 +32,7 @@ export const globalTypes = {
       icon: 'globe',
       items: [
         { value: 'en', right: '🇺🇸', title: 'English' },
-        { value: 'es', right: '🇪🇸', title: 'Spanish' },
+        { value: 'de', right: '🇪🇸', title: 'German' },
       ],
     },
   },
@@ -58,7 +52,7 @@ export const decorators = [
             <Story />
           </div>
         } else {
-          const path = !isTestingHeader ? /popup(\/.*)\/.*/.exec(kind)[1] : ''
+          const path = !isTestingHeader ? /popup(\/.*).*/.exec(kind)[1] : ''
           // add a fake header so it looks similar
           return <Fragment>
             <NavBar path={path} devMode={path === '/dev'} />
@@ -113,9 +107,7 @@ export const decorators = [
       <Story />
     </div>
   },
-  (Story, { globals }) => {
-    setupI18n(globals.locale, strings);
-    return <Story />
-  },
-  //   (Story) => <ConfigContextProvider value={mockConfig}> <Story /> 
</ConfigContextProvider>
+  (Story, { globals }) => <TranslationProvider initial='en' 
forceLang={globals.locale}>
+    <Story />
+  </TranslationProvider>,
 ];
diff --git a/packages/taler-wallet-webextension/clean_and_build.sh 
b/packages/taler-wallet-webextension/clean_and_build.sh
index e862be37..fb8b31c7 100755
--- a/packages/taler-wallet-webextension/clean_and_build.sh
+++ b/packages/taler-wallet-webextension/clean_and_build.sh
@@ -1,5 +1,6 @@
 #!/usr/bin/env bash
 # This file is in the public domain.
 [ "also-wallet" == "$1" ] && { pnpm -C ../taler-wallet-core/ compile || exit 
1; }
+[ "also-util" == "$1" ] && { pnpm -C ../taler-util/ prepare || exit 1; }
 pnpm clean && pnpm compile && rm -rf extension/ && ./pack.sh  && (cd 
extension/ && unzip taler*.zip)
 
diff --git a/packages/taler-wallet-webextension/src/components/SelectList.tsx 
b/packages/taler-wallet-webextension/src/components/SelectList.tsx
new file mode 100644
index 00000000..2c4a106e
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/SelectList.tsx
@@ -0,0 +1,40 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems SA
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { VNode } from "preact";
+import { useRef, useState } from "preact/hooks";
+import { JSX } from "preact/jsx-runtime";
+
+interface Props {
+  value: string;
+  onChange: (s: string) => void;
+  label: string;
+  list: {
+    [label: string]: string
+  }
+  name: string;
+  description?: string;
+}
+
+export function SelectList({ name, value, list, onChange, label, description 
}: Props): JSX.Element {
+  return <select name={name} id="slct">
+    <option selected disabled>Choose an option</option>
+    {Object.keys(list)
+      .filter((l) => l !== value)
+      .map(key => <option value={key} key={key}>{list[key]}</option> )
+    }
+  </select>
+}
diff --git a/packages/taler-wallet-webextension/src/context/useDevContext.ts 
b/packages/taler-wallet-webextension/src/context/devContext.ts
similarity index 100%
rename from packages/taler-wallet-webextension/src/context/useDevContext.ts
rename to packages/taler-wallet-webextension/src/context/devContext.ts
diff --git a/packages/taler-wallet-webextension/src/context/translation.ts 
b/packages/taler-wallet-webextension/src/context/translation.ts
new file mode 100644
index 00000000..5f57958d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/context/translation.ts
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { createContext, h, VNode } from 'preact'
+import { useContext, useEffect } from 'preact/hooks'
+import { useLang } from '../hooks/useLang'
+//@ts-ignore: type declaration
+import * as jedLib from "jed";
+import { strings } from "../i18n/strings";
+import { setupI18n } from '@gnu-taler/taler-util';
+
+interface Type {
+  lang: string;
+  changeLanguage: (l: string) => void;
+}
+const initial = {
+  lang: 'en',
+  changeLanguage: () => {
+    // do not change anything
+  }
+}
+const Context = createContext<Type>(initial)
+
+interface Props {
+  initial?: string,
+  children: any,
+  forceLang?: string
+}
+
+//we use forceLang when we don't want to use the saved state, but sone forced 
+//runtime lang predefined lang 
+export const TranslationProvider = ({ initial, children, forceLang }: Props): 
VNode => {
+  const [lang, changeLanguage] = useLang(initial)
+  useEffect(() => {
+    if (forceLang) {
+      changeLanguage(forceLang)
+    }
+  })
+  useEffect(()=> {
+    setupI18n(lang, strings)
+  },[lang])
+  if (forceLang) {
+    setupI18n(forceLang, strings)
+  } else {
+    setupI18n(lang, strings)    
+  }
+  return h(Context.Provider, { value: { lang, changeLanguage }, children });
+}
+
+export const useTranslationContext = (): Type => useContext(Context);
diff --git a/packages/taler-wallet-webextension/src/hooks/useLang.ts 
b/packages/taler-wallet-webextension/src/hooks/useLang.ts
new file mode 100644
index 00000000..d9ad7cd5
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/hooks/useLang.ts
@@ -0,0 +1,7 @@
+import { useNotNullLocalStorage } from './useLocalStorage';
+
+export function useLang(initial?: string): [string, (s:string) => void] {
+  const browserLang: string | undefined = typeof window !== "undefined" ? 
navigator.language || (navigator as any).userLanguage : undefined;
+  const defaultLang = (browserLang || initial || 'en').substring(0, 2)
+  return useNotNullLocalStorage('lang-preference', defaultLang)
+}
diff --git a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts 
b/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
index 30f68194..78a8b65d 100644
--- a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
@@ -42,3 +42,24 @@ export function useLocalStorage(key: string, initialValue?: 
string): [string | u
 
   return [storedValue, setValue];
 }
+
+//TODO: merge with the above function
+export function useNotNullLocalStorage(key: string, initialValue: string): 
[string, StateUpdater<string>] {
+  const [storedValue, setStoredValue] = useState<string>((): string => {
+    return typeof window !== "undefined" ? window.localStorage.getItem(key) || 
initialValue : initialValue;
+  });
+
+  const setValue = (value: string | ((val: string) => string)) => {
+    const valueToStore = value instanceof Function ? value(storedValue) : 
value;
+    setStoredValue(valueToStore);
+    if (typeof window !== "undefined") {
+      if (!valueToStore) {
+        window.localStorage.removeItem(key)
+      } else {
+        window.localStorage.setItem(key, valueToStore);
+      }
+    }
+  };
+
+  return [storedValue, setValue];
+}
diff --git a/packages/taler-wallet-webextension/src/i18n/strings.ts 
b/packages/taler-wallet-webextension/src/i18n/strings.ts
index 748b9656..5b125783 100644
--- a/packages/taler-wallet-webextension/src/i18n/strings.ts
+++ b/packages/taler-wallet-webextension/src/i18n/strings.ts
@@ -159,6 +159,76 @@ strings["en-US"] = {
   },
 };
 
+strings["es"] = {
+  domain: "messages",
+  locale_data: {
+    messages: {
+      "": {
+        domain: "messages",
+        plural_forms: "nplurals=2; plural=(n != 1);",
+        lang: "",
+      },
+      "Invalid Wire": [""],
+      "Invalid Test Wire Detail": [""],
+      "Test Wire Acct #%1$s on %2$s": [""],
+      "Unknown Wire Detail": [""],
+      Operation: [""],
+      "time (ms/op)": [""],
+      "The merchant %1$s offers you to purchase:": [""],
+      "The total price is %1$s (plus %2$s fees).": [""],
+      "The total price is %1$s.": [""],
+      Retry: [""],
+      "Confirm payment": [""],
+      Balance: [""],
+      History: ["Historial"],
+      Debug: [""],
+      "You have no balance to show. Need some %1$s getting started?": [""],
+      "%1$s incoming": [""],
+      "%1$s being spent": [""],
+      "Error: could not retrieve balance information.": [""],
+      "Invalid ": [""],
+      "Fees ": [""],
+      "Refresh sessions has completed": [""],
+      "Order Refused": [""],
+      "Order redirected": [""],
+      "Payment aborted": [""],
+      "Payment Sent": [""],
+      "Backup": ["Resguardo"],
+      "Order accepted": [""],
+      "Reserve balance updated": [""],
+      "Payment refund": [""],
+      Withdrawn: [""],
+      "Tip Accepted": [""],
+      "Tip Declined": [""],
+      "%1$s": [""],
+      "Your wallet has no events recorded.": [""],
+      "Wire to bank account": [""],
+      Confirm: ["Confirmar"],
+      Cancel: ["Cancelar"],
+      "Could not get details for withdraw operation:": [""],
+      "Chose different exchange provider": [""],
+      "Please select an exchange.  You can review the details before after 
your selection.": [
+        "",
+      ],
+      "Select %1$s": [""],
+      "Select custom exchange": [""],
+      "You are about to withdraw %1$s from your bank account into your 
wallet.": [
+        "",
+      ],
+      "Accept fees and withdraw": [""],
+      "Cancel withdraw operation": [""],
+      "Withdrawal fees:": [""],
+      "Rounding loss:": [""],
+      "Earliest expiration (for deposit): %1$s": [""],
+      "# Coins": [""],
+      Value: [""],
+      "Withdraw Fee": [""],
+      "Refresh Fee": [""],
+      "Deposit Fee": [""],
+    },
+  },
+};
+
 strings["fr"] = {
   domain: "messages",
   locale_data: {
diff --git 
a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx 
b/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx
index 0b76d756..30512d22 100644
--- a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx
@@ -31,7 +31,7 @@ interface Props {
 export function ProviderDetailPage({ pid, onBack }: Props): VNode {
   const status = useProviderStatus(pid)
   if (!status) {
-    return <div>Loading...</div>
+    return <div><i18n.Translate>Loading...</i18n.Translate></div>
   }
   if (!status.info) {
     onBack()
@@ -67,26 +67,26 @@ export function ProviderView({ info, onDelete, onSync, 
onBack, onExtend }: ViewP
         <p>{daysSince(info?.lastSuccessfulBackupTimestamp)} </p>
         <p>{descriptionByStatus(info.paymentStatus)}</p>
         {info.paymentStatus.type === ProviderPaymentType.TermsChanged && <div>
-          <p>terms has changed, extending the service will imply accepting the 
new terms of service</p>
+          <p><i18n.Translate>terms has changed, extending the service will 
imply accepting the new terms of service</i18n.Translate></p>
           <table>
             <thead>
               <tr>
                 <td></td>
-                <td>old</td>
+                <td><i18n.Translate>old</i18n.Translate></td>
                 <td> -&gt;</td>
-                <td>new</td>
+                <td><i18n.Translate>new</i18n.Translate></td>
               </tr>
             </thead>
             <tbody>
 
               <tr>
-                <td>fee</td>
+                <td><i18n.Translate>fee</i18n.Translate></td>
                 <td>{info.paymentStatus.oldTerms.annualFee}</td>
                 <td>-&gt;</td>
                 <td>{info.paymentStatus.newTerms.annualFee}</td>
               </tr>
               <tr>
-                <td>storage</td>
+                <td><i18n.Translate>storage</i18n.Translate></td>
                 <td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
                 <td>-&gt;</td>
                 <td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
@@ -117,11 +117,11 @@ function daysSince(d?: Timestamp) {
   const str = formatDuration(duration, {
     delimiter: ', ',
     format: [
-      duration?.years ? 'years' : (
-        duration?.months ? 'months' : (
-          duration?.days ? 'days' : (
-            duration?.hours ? 'hours' : (
-              duration?.minutes ? 'minutes' : 'seconds'
+      duration?.years ? i18n.str`years` : (
+        duration?.months ? i18n.str`months` : (
+          duration?.days ? i18n.str`days` : (
+            duration?.hours ? i18n.str`hours` : (
+              duration?.minutes ? i18n.str`minutes` : i18n.str`seconds`
             )
           )
         )
@@ -139,13 +139,13 @@ function Error({ info }: { info: ProviderInfo }) {
     switch (info.backupProblem.type) {
       case "backup-conflicting-device":
         return <ErrorMessage title={<Fragment>
-          There is conflict with another backup from 
<b>{info.backupProblem.otherDeviceId}</b>
+          <i18n.Translate>There is conflict with another backup from 
<b>{info.backupProblem.otherDeviceId}</b></i18n.Translate>
         </Fragment>} />
       case "backup-unreadable":
         return <ErrorMessage title="Backup is not readable" />
       default:
         return <ErrorMessage title={<Fragment>
-          Unknown backup problem: {JSON.stringify(info.backupProblem)}
+          <i18n.Translate>Unknown backup problem: 
{JSON.stringify(info.backupProblem)}</i18n.Translate>
         </Fragment>} />
     }
   }
@@ -172,15 +172,15 @@ function colorByStatus(status: ProviderPaymentType) {
 function descriptionByStatus(status: ProviderPaymentStatus) {
   switch (status.type) {
     case ProviderPaymentType.InsufficientBalance:
-      return 'no enough balance to make the payment'
+      return i18n.str`no enough balance to make the payment`
     case ProviderPaymentType.Unpaid:
-      return 'not paid yet'
+      return i18n.str`not paid yet`
     case ProviderPaymentType.Paid:
     case ProviderPaymentType.TermsChanged:
       if (status.paidUntil.t_ms === 'never') {
-        return 'service paid.'
+        return i18n.str`service paid`
       } else {
-        return `service paid until ${format(status.paidUntil.t_ms, 'yyyy/MM/dd 
HH:mm:ss')}`
+        return i18n.str`service paid until ${format(status.paidUntil.t_ms, 
'yyyy/MM/dd HH:mm:ss')}`
       }
     case ProviderPaymentType.Pending:
       return ''
diff --git a/packages/taler-wallet-webextension/src/popup/Settings.tsx 
b/packages/taler-wallet-webextension/src/popup/Settings.tsx
index d8cd0438..9bb10a4e 100644
--- a/packages/taler-wallet-webextension/src/popup/Settings.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Settings.tsx
@@ -15,18 +15,23 @@
 */
 
 
+import { i18n } from "@gnu-taler/taler-util";
 import { VNode } from "preact";
 import { Checkbox } from "../components/Checkbox";
 import { EditableText } from "../components/EditableText";
-import { useDevContext } from "../context/useDevContext";
+import { SelectList } from "../components/SelectList";
+import { useDevContext } from "../context/devContext";
 import { useBackupDeviceName } from "../hooks/useBackupDeviceName";
 import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
+import { useLang } from "../hooks/useLang";
 
 export function SettingsPage(): VNode {
   const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
   const { devMode, toggleDevMode } = useDevContext()
   const { name, update } = useBackupDeviceName()
+  const [lang, changeLang] = useLang()
   return <SettingsView
+    lang={lang} changeLang={changeLang}
     deviceName={name} setDeviceName={update}
     permissionsEnabled={permissionsEnabled} 
togglePermissions={togglePermissions}
     developerMode={devMode} toggleDeveloperMode={toggleDevMode}
@@ -34,6 +39,8 @@ export function SettingsPage(): VNode {
 }
 
 export interface ViewProps {
+  lang: string;
+  changeLang: (s: string) => void;
   deviceName: string;
   setDeviceName: (s: string) => Promise<void>;
   permissionsEnabled: boolean;
@@ -42,20 +49,43 @@ export interface ViewProps {
   toggleDeveloperMode: () => void;
 }
 
-export function SettingsView({ deviceName, setDeviceName, permissionsEnabled, 
togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode {
+import { strings as messages } from '../i18n/strings'
+
+type LangsNames = {
+  [P in keyof typeof messages]: string
+}
+
+const names: LangsNames = {
+  es: 'Español [es]',
+  en: 'English [en]',
+  fr: 'Français [fr]',
+  de: 'Deutsch [de]',
+  sv: 'Svenska [sv]',
+  it: 'Italiano [it]',
+}
+
+
+export function SettingsView({ lang, changeLang, deviceName, setDeviceName, 
permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: 
ViewProps): VNode {
   return (
     <div>
       <section style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' 
}}>
-
-        <h2>Wallet</h2>
+        <h2><i18n.Translate>Wallet</i18n.Translate></h2>
+        <SelectList
+          value={lang}
+          onChange={changeLang}
+          name="lang"
+          list={names}
+          label={i18n.str`Lang`}
+          description="(Choose your preferred lang)"
+        />
         <EditableText
           value={deviceName}
           onChange={setDeviceName}
           name="device-id"
-          label="Device name"
+          label={i18n.str`Device name`}
           description="(This is how you will recognize the wallet in the 
backup provider)"
         />
-        <h2>Permissions</h2>
+        <h2><i18n.Translate>Permissions</i18n.Translate></h2>
         <Checkbox label="Automatically open wallet based on page content"
           name="perm"
           description="(Enabling this option below will make using the wallet 
faster, but requires more permissions from your browser.)"
diff --git a/packages/taler-wallet-webextension/src/popup/popup.tsx 
b/packages/taler-wallet-webextension/src/popup/popup.tsx
index 32ff10a8..a6be4d19 100644
--- a/packages/taler-wallet-webextension/src/popup/popup.tsx
+++ b/packages/taler-wallet-webextension/src/popup/popup.tsx
@@ -27,7 +27,7 @@
 import { i18n } from "@gnu-taler/taler-util";
 import { ComponentChildren, JSX } from "preact";
 import Match from "preact-router/match";
-import { useDevContext } from "../context/useDevContext";
+import { useDevContext } from "../context/devContext";
 import { PopupNavigation } from '../components/styled'
 
 export enum Pages {
diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx 
b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
index 613218b8..39c25d50 100644
--- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
+++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
@@ -25,7 +25,7 @@ import { createHashHistory } from "history";
 import { render } from "preact";
 import Router, { route, Route } from "preact-router";
 import { useEffect } from "preact/hooks";
-import { DevContextProvider } from "./context/useDevContext";
+import { DevContextProvider } from "./context/devContext";
 import { useTalerActionURL } from "./hooks/useTalerActionURL";
 import { strings } from "./i18n/strings";
 import { BackupPage } from "./popup/BackupPage";

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]