Skip to content

Commit

Permalink
feat(TSE-1144): Add SSR possibility on Next (#102)
Browse files Browse the repository at this point in the history
Co-authored-by: Ashley Hsiao <ashley.hsiao@phrase.com>
  • Loading branch information
Varpuspaavi and itsahsiao authored Sep 6, 2023
1 parent 34af114 commit 6bac6e5
Show file tree
Hide file tree
Showing 21 changed files with 3,409 additions and 12 deletions.
3 changes: 3 additions & 0 deletions examples/next-ssr-demo/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
43 changes: 43 additions & 0 deletions examples/next-ssr-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# Yarn
.yarn/*
*/.yarn
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
12 changes: 12 additions & 0 deletions examples/next-ssr-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). It shows how to integrate the [PhraseApp In-Context Editor](https://phraseapp.com/) using [react-intl](https://github.com/yahoo/react-intl) for localization via the [react-intl-phraseapp](https://github.com/phrase/react-intl-phraseapp) plugin.

## Getting Started

First, run the development server:

```bash
yarn && yarn dev
```

Open [http://localhost:3000/client](http://localhost:3000/client) with your browser to see the client component demo
Open [http://localhost:3000/server](http://localhost:3000/server) with your browser to see the server component demo
8 changes: 8 additions & 0 deletions examples/next-ssr-demo/next.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
}

module.exports = nextConfig
24 changes: 24 additions & 0 deletions examples/next-ssr-demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "next-ssr-demo",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@types/node": "20.5.9",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"eslint": "8.48.0",
"eslint-config-next": "13.4.19",
"next": "13.4.18",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-intl": "^6.4.4",
"react-intl-phraseapp": "link:../../dist",
"typescript": "5.2.2"
}
}
10 changes: 10 additions & 0 deletions examples/next-ssr-demo/src/app/client/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.App {
text-align: center;
padding: 32px;
}


body > iframe
{
display: none;
}
30 changes: 30 additions & 0 deletions examples/next-ssr-demo/src/app/client/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client'

import './App.css';
import { FormattedMessage, useIntl } from 'react-intl-phraseapp';
import ClassTestComponent from './ClassTestComponent';

const App = () => {
const { formatMessage } = useIntl();

return (
<div className="App">
<h2>This is a simple demo of react-intl Next.js client integration of the In-Context Editor</h2>
<p>
{/* one can use FormattedMessage component */}
<FormattedMessage
id="hero_title"
defaultMessage={`We hope this example will help you integrate PhraseApp into your react app using react-intl`}
/>
</p>
<p>
{/* one can use formatMessage from useIntl hook */}
{ formatMessage({ id: 'create_this_key', defaultMessage: 'Uncreated key to show creation capabilities.' }) }
</p>

<ClassTestComponent translation="advantages_text" />
</div>
);
}

export default App;
39 changes: 39 additions & 0 deletions examples/next-ssr-demo/src/app/client/ClassTestComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client'

import { Component } from 'react';
import { FormattedMessage, injectIntl, WrappedComponentProps } from 'react-intl-phraseapp';

type Props = WrappedComponentProps & { translation: string };

class ClassTestComponent extends Component<Props> {
render() {
return (
<div>
<h3>Class component:</h3>
<p>
{/* one can use Formatted Message */}
<FormattedMessage
id="integrate_text"
defaultMessage={`We hope this example will help you integrate PhraseApp into your react app using react-intl`}
/>
</p>
<p>
<FormattedMessage
id="create_this_key"
defaultMessage={`Uncreated key to show creation capabilities.`}
/>
</p>
<p>
{/* or use HOC injectIntl wrapper to use the intl.formatMessage prop
WrappedComponentProps must be typed with the component's props! */}
{this.props.intl.formatMessage({ id: this.props.translation })}
</p>
<p>
{this.props.intl.formatMessage({id: 'variable_text'}, {variable: <b>SomeVariableForTest</b>})}
</p>
</div>
);
}
}

export default injectIntl(ClassTestComponent);
35 changes: 35 additions & 0 deletions examples/next-ssr-demo/src/app/client/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client'

import './App.css';
import { initializePhraseAppEditor } from 'react-intl-phraseapp';
import { IntlProvider } from 'react-intl';
import App from './App';

export const page = () => {
const config = {
projectId: '00000000000000004158e0858d2fa45c',
accountId: '0bed59e5',
phraseEnabled: true,
prefix: '[[__',
suffix: '__]]'
}

const messages = {
hero_title: "Enable the ICE by setting `phraseEnabled: true`!",
advantages_text: "With the In-context Editor, clients are able to minimize errors by giving translators insight into the context of the translation and letting them edit the content right on the spot.",
integrate_title: "Integrate the Phrase In-context Editor into your existing application",
create_this_key: "This key doesn't exist yet, try creating it!",
integrate_text: "We hope this example will help you integrate PhraseApp into your react app using react-intl",
variable_text: "{variable} variable should show up when ICE is not enabled!"
}

initializePhraseAppEditor(config);

return (
<IntlProvider locale="en" messages={messages}>
<App/>
</IntlProvider>
);
}

export default page;
21 changes: 21 additions & 0 deletions examples/next-ssr-demo/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
10 changes: 10 additions & 0 deletions examples/next-ssr-demo/src/app/server/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.App {
text-align: center;
padding: 32px;
}


body > iframe
{
display: none;
}
34 changes: 34 additions & 0 deletions examples/next-ssr-demo/src/app/server/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'server-only'

import './App.css';
import { getIntl } from './utils';

export default async function App() {
const { formatMessage } = getIntl();
return (
<div className="App">
<h2>This is a simple demo of react-intl Next.js SSR integration of the In-Context Editor</h2>
<p>
{/* FormattedMessage cannot be used because of internal context use that is not available on server components */}
</p>
<p>
{/* One can use formatMessage as defined by getIntl */}
{ formatMessage({ id: 'hero_title'}) }
</p>
<p>
{ formatMessage({ id: 'advantages_text'}) }
</p>
<p>
{ formatMessage({ id: 'create_this_key'}) }
</p>
<p>
{ formatMessage({ id: 'integrate_text'}) }
</p>
<p>
{ formatMessage({ id: 'variable_text'}) }
</p>

{/* Class components cannot be SSR */}
</div>
);
}
24 changes: 24 additions & 0 deletions examples/next-ssr-demo/src/app/server/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'server-only'

import './App.css';
import App from './App';
import Script from 'next/script';
import { createPhraseAppEditorScript } from 'react-intl-phraseapp/functions';

export default async function Page() {
// Adjust your Project and Account ids
// Basically you can use ICE with anything as long as you can manage to convert your translation library's key output to [[__phrase_some.key.id__]]
// This format will allow ICE to catch the keys and should then be able to process them
const config = {
projectId: '00000000000000004158e0858d2fa45c',
accountId: '0bed59e5',

}

return (
<div>
<App/>
<Script id="phrase-script" dangerouslySetInnerHTML={{__html: createPhraseAppEditorScript(config)}} />
</div>
);
}
31 changes: 31 additions & 0 deletions examples/next-ssr-demo/src/app/server/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'server-only'
// import { IntlShape, createIntl, createIntlCache } from "@formatjs/intl"; NOTE the @formatjs/intl, not react-intl
import { useSSRIntl as initIntl } from "react-intl-phraseapp/useSSRIntl"; // If you import "react-intl-phraseapp" you will get useContext related error

export function getIntl() {
const locale = 'en'
const messages = {
hero_title: "Enable the ICE by setting `phraseEnabled: true`!",
advantages_text: "With the In-context Editor, clients are able to minimize errors by giving translators insight into the context of the translation and letting them edit the content right on the spot.",
integrate_title: "Integrate the Phrase In-context Editor into your existing application",
create_this_key: "This key doesn't exist yet, try creating it!",
integrate_text: "We hope this example will help you integrate PhraseApp into your react app using react-intl",
variable_text: "{variable} variable should show up when ICE is not enabled!"
}

const intl = initIntl({locale, messages})

// Or you can do something like this
// const cache = createIntlCache();
// const intl = {
// ...createIntl({locale, messages}, cache),
// // Add condition here to overwrite formatMessage with this format when needed
// formatMessage: (
// messageDescriptor: Parameters<IntlShape['formatMessage']>[0],
// _values?: Parameters<IntlShape['formatMessage']>[1],
// _opts?: Parameters<IntlShape['formatMessage']>[2]
// ) => "[[__phrase_" + messageDescriptor.id + "__]]"
// }

return intl
}
41 changes: 41 additions & 0 deletions examples/next-ssr-demo/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
]
},
"forceConsistentCasingInFileNames": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
Loading

0 comments on commit 6bac6e5

Please sign in to comment.