Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Public dir support #569

Merged
merged 16 commits into from
Dec 24, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,21 @@ You need to also have `react` and `next` installed.
By default, `next-i18next` expects your translations to be organised as such:
```
.
└── static
└── locales
├── en
| └── common.json
└── de
└── common.json
└── public
└── static
└── locales
├── en
| └── common.json
└── de
└── common.json
```

This structure can also be seen in the [simple example](./examples/simple).

If you want to structure your translations/namespaces in a custom way, you will need to pass modified `localePath` and `localeStructure` values into the initialisation config.

If translations are not found in `config.localePath` or `public/static/locales` an attempt will be made to find the locales in `static/locales`, if found a deprecation warning will be logged.

### 3. Project setup

The default export of `next-i18next` is a class constructor, into which you pass your config options. The resulting class has all the methods you will need to translate your app:
Expand Down Expand Up @@ -234,10 +237,10 @@ MyPage.getInitialProps = async({ req }) => {
| `browserLanguageDetection` | `true` |
| `defaultNS` | `'common'` |
| `defaultLanguage` | `'en'` |
| `ignoreRoutes` | `['/_next/', '/static/']` |
| `ignoreRoutes` | `['/_next/', '/static/', '/public/']` |
| `otherLanguages` (required) | `[]` |
| `localeExtension` | `'json'` |
| `localePath` | `'static/locales'` |
| `localePath` | `'public/static/locales'` |
| `localeStructure` | `'{{lng}}/{{ns}}'` |
| `localeSubpaths` | `{}` |
| `serverLanguageDetection` | `true` |
Expand Down
39 changes: 26 additions & 13 deletions __tests__/config/create-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('create configuration in non-production environment', () => {
}))

expect(() => createConfig({})).toThrow(
'Default namespace not found at /home/user/static/locales/en/common.json',
'Default namespace not found at /home/user/public/static/locales/en/common.json',
)
})

Expand All @@ -64,7 +64,7 @@ describe('create configuration in non-production environment', () => {
expect(config.otherLanguages).toEqual([])
expect(config.fallbackLng).toEqual(false)
expect(config.load).toEqual('currentOnly')
expect(config.localePath).toEqual('static/locales')
expect(config.localePath).toEqual('public/static/locales')
expect(config.localeStructure).toEqual('{{lng}}/{{ns}}')
expect(config.localeSubpaths).toEqual({})
expect(config.use).toEqual([])
Expand All @@ -88,8 +88,8 @@ describe('create configuration in non-production environment', () => {

expect(config.ns).toEqual(['common', 'file1', 'file2'])

expect(config.backend.loadPath).toEqual('/home/user/static/locales/{{lng}}/{{ns}}.json')
expect(config.backend.addPath).toEqual('/home/user/static/locales/{{lng}}/{{ns}}.missing.json')
expect(config.backend.loadPath).toEqual('/home/user/public/static/locales/{{lng}}/{{ns}}.json')
expect(config.backend.addPath).toEqual('/home/user/public/static/locales/{{lng}}/{{ns}}.missing.json')
})

it('creates custom non-production configuration', () => {
Expand All @@ -104,7 +104,7 @@ describe('create configuration in non-production environment', () => {
expect(config.otherLanguages).toEqual(['fr', 'it'])
expect(config.fallbackLng).toEqual('it')
expect(config.load).toEqual('currentOnly')
expect(config.localePath).toEqual('static/translations')
expect(config.localePath).toEqual('public/static/translations')
expect(config.localeStructure).toEqual('{{ns}}/{{lng}}')
expect(config.localeSubpaths).toEqual(localeSubpathVariations.FOREIGN)
expect(config.defaultNS).toEqual('universal')
Expand All @@ -113,8 +113,21 @@ describe('create configuration in non-production environment', () => {

expect(config.ns).toEqual(['universal', 'file1', 'file2'])

expect(config.backend.loadPath).toEqual('/home/user/static/translations/{{ns}}/{{lng}}.json')
expect(config.backend.addPath).toEqual('/home/user/static/translations/{{ns}}/{{lng}}.missing.json')
expect(config.backend.loadPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.json')
expect(config.backend.addPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.missing.json')
})

it('falls back to deprecated static folder', () => {
isServer.mockReturnValue(true)
evalFunc.mockImplementation(() => ({
readdirSync: jest.fn().mockImplementation(() => ['universal', 'file1', 'file2']),
existsSync: jest.fn().mockImplementationOnce(() => false).mockImplementationOnce(() => true),
}))
const config = createConfig({ localePath: 'bogus/path' })

expect(config.backend.loadPath).toEqual('/home/user/static/locales/{{lng}}/{{ns}}.json')
expect(config.backend.addPath).toEqual('/home/user/static/locales/{{lng}}/{{ns}}.missing.json')
expect(config.ns).toEqual(['universal', 'file1', 'file2'])
})

it('preserves config.ns, if provided in user configuration', () => {
Expand All @@ -132,16 +145,16 @@ describe('create configuration in non-production environment', () => {
describe('localeExtension config option', () => {
it('is set to JSON by default', () => {
const config = createConfig(userConfig)
expect(config.backend.loadPath).toEqual('/home/user/static/translations/{{ns}}/{{lng}}.json')
expect(config.backend.addPath).toEqual('/home/user/static/translations/{{ns}}/{{lng}}.missing.json')
expect(config.backend.loadPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.json')
expect(config.backend.addPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.missing.json')
})
it('accepts any string and modifies backend paths', () => {
const config = createConfig({
...userConfig,
localeExtension: 'test-extension',
})
expect(config.backend.loadPath).toEqual('/home/user/static/translations/{{ns}}/{{lng}}.test-extension')
expect(config.backend.addPath).toEqual('/home/user/static/translations/{{ns}}/{{lng}}.missing.test-extension')
expect(config.backend.loadPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.test-extension')
expect(config.backend.addPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.missing.test-extension')
})
})
})
Expand All @@ -155,7 +168,7 @@ describe('create configuration in non-production environment', () => {
expect(config.otherLanguages).toEqual([])
expect(config.fallbackLng).toEqual(false)
expect(config.load).toEqual('currentOnly')
expect(config.localePath).toEqual('static/locales')
expect(config.localePath).toEqual('public/static/locales')
expect(config.localeStructure).toEqual('{{lng}}/{{ns}}')
expect(config.localeSubpaths).toEqual(localeSubpathVariations.NONE)
expect(config.use).toEqual([])
Expand Down Expand Up @@ -190,7 +203,7 @@ describe('create configuration in non-production environment', () => {
expect(config.otherLanguages).toEqual(['fr', 'it'])
expect(config.fallbackLng).toEqual('it')
expect(config.load).toEqual('currentOnly')
expect(config.localePath).toEqual('static/translations')
expect(config.localePath).toEqual('public/static/translations')
expect(config.localeStructure).toEqual('{{ns}}/{{lng}}')
expect(config.localeSubpaths).toEqual(localeSubpathVariations.FOREIGN)
expect(config.defaultNS).toEqual('universal')
Expand Down
6 changes: 3 additions & 3 deletions __tests__/config/test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const userConfig = {
defaultNS: 'universal',
fallbackLng: 'it',
otherLanguages: ['fr', 'it'],
localePath: 'static/translations',
localePath: 'public/static/translations',
localeStructure: '{{ns}}/{{lng}}',
localeSubpaths: localeSubpathVariations.FOREIGN,
}
Expand All @@ -31,8 +31,8 @@ const userConfigClientSide = {
const userConfigServerSide = {
...userConfig,
backend: {
loadPath: '/home/user/static/translations/{{ns}}/{{lng}}.json',
addPath: '/home/user/static/translations/{{ns}}/{{lng}}.missing.json',
loadPath: '/home/user/public/static/translations/{{ns}}/{{lng}}.json',
addPath: '/home/user/public/static/translations/{{ns}}/{{lng}}.missing.json',
},
}

Expand Down
2 changes: 1 addition & 1 deletion __tests__/test-i18next-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default {
allLanguages: ['en', 'de'],
defaultLanguage: 'en',
otherLanguages: ['de'],
ignoreRoutes: ['/_next/', '/static/'],
ignoreRoutes: ['/_next/', '/static/', '/public/'],
serverLanguageDetection: true,
},
}
89 changes: 89 additions & 0 deletions examples/simple/public/static/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#__next {
font-family: 'Open Sans', sans-serif;
text-align: center;
background-image: linear-gradient(to left top, #ffffff, #f5f5f5, #eaeaea, #e0e0e0, #d6d6d6);
display: flex;
flex-direction: column;
min-height: 100vh;
min-width: 100vw;
}

h1,
h2 {
font-family: 'Oswald', sans-serif;
}

h1 {
font-size: 3rem;
margin: 5rem 0;
}
h2 {
min-width: 18rem;
font-size: 2rem;
opacity: 0.3;
}

p {
line-height: 1.65em;
}
p:nth-child(2) {
font-style: italic;
opacity: 0.65;
margin-top: 1rem;
}

a.github {
position: fixed;
top: 0.5rem;
right: 0.75rem;
font-size: 4rem;
color: #888;
opacity: 0.8;
}
a.github:hover {
opacity: 1;
}

button {
display: inline-block;
vertical-align: bottom;
outline: 0;
text-decoration: none;
cursor: pointer;
padding: .4rem;
background-color: rgba(255, 255, 255, 0.5);
box-sizing: border-box;
font-size: 1em;
font-family: inherit;
border-radius: 3px;
margin: .1rem;
transition: box-shadow .2s ease;
user-select: none;
line-height: 2.5em;
min-height: 40px;
padding: 0 .8em;
border: 0;
color: inherit;
position: relative;
transform: translateZ(0);
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .26);
margin: 0.8rem;
}

button:hover,
button:focus {
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, .4);
}

main {
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
align-items: center;
}
footer {
background-color: rgba(255, 255, 255, 0.5);
width: 100vw;
padding: 3rem 0;
}
7 changes: 7 additions & 0 deletions examples/simple/public/static/locales/de/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"h1": "Ein einfaches Beispiel",
"change-locale": "Wechseln Locale",
"to-second-page": "Zur zweiten Seite",
"error-with-status": "Auf dem Server ist ein Fehler ({{statusCode}}) aufgetreten",
"error-without-status": "Auf dem Server ist ein Fehler aufgetreten"
}
3 changes: 3 additions & 0 deletions examples/simple/public/static/locales/de/footer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"description": "Dies ist eine Nicht-Seitenkomponente, die einen eigenen Namespace erfordert"
}
4 changes: 4 additions & 0 deletions examples/simple/public/static/locales/de/second-page.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"h1": "Eine zweite Seite, um das Routing zu demonstrieren",
"back-to-home": "Zurück zur Hauptseite"
}
7 changes: 7 additions & 0 deletions examples/simple/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"h1": "A simple example",
"change-locale": "Change locale",
"to-second-page": "To second page",
"error-with-status": "A {{statusCode}} error occurred on server",
"error-without-status": "An error occurred on the server"
}
3 changes: 3 additions & 0 deletions examples/simple/public/static/locales/en/footer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"description": "This is a non-page component that requires its own namespace"
}
4 changes: 4 additions & 0 deletions examples/simple/public/static/locales/en/second-page.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"h1": "A second page, to demonstrate routing",
"back-to-home": "Back to home"
}
34 changes: 26 additions & 8 deletions src/config/create-config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { defaultConfig } from './default-config'
import { isServer } from '../utils/is-server'
import { consoleMessage, isServer } from '../utils'

const deepMergeObjects = ['backend', 'detection']
const dedupe = (names: string[]) => names.filter((v,i) => names.indexOf(v) === i)
const STATIC_LOCALE_PATH = 'static/locales'

export const createConfig = (userConfig) => {

Expand Down Expand Up @@ -33,36 +34,53 @@ export const createConfig = (userConfig) => {

const fs = eval("require('fs')")
const path = require('path')
let serverLocalePath = localePath

// Validate defaultNS
// https://github.com/isaachinman/next-i18next/issues/358
if (process.env.NODE_ENV !== 'production' && typeof combinedConfig.defaultNS === 'string') {
const defaultNSPath = path.join(process.cwd(), `${localePath}/${defaultLanguage}/${combinedConfig.defaultNS}.${localeExtension}`)
const defaultFile = `/${defaultLanguage}/${combinedConfig.defaultNS}.${localeExtension}`
const defaultNSPath = path.join(process.cwd(), localePath, defaultFile)
const defaultNSExists = fs.existsSync(defaultNSPath)
if (!defaultNSExists) {
throw new Error(`Default namespace not found at ${defaultNSPath}`)
// if defaultNS doesn't exist, try to fall back to the deprecated static folder
// https://github.com/isaachinman/next-i18next/issues/523
const staticDirPath = path.join(process.cwd(), STATIC_LOCALE_PATH, defaultFile)
const staticDirExists = fs.existsSync(staticDirPath)
if (staticDirExists) {
consoleMessage('warn', 'Falling back to /static folder, deprecated in next@9.1.*', combinedConfig)
serverLocalePath = STATIC_LOCALE_PATH
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am just now realising that this fallback will only take place in non-production modes. Wouldn't that effectively break all currently-deployed apps where people rely on the default value of localePath being the static dir?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you're right, will look at this again in the morning.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the break, holidays and work and what not. What was the original reason for the process.env.NODE_ENV !== 'production' here? Was it to prevent the error throw in production? I looked at the ticket in the comment and the PR for it and just want to figure out the original intent before changing anything more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem. Yes that's correct - this if block is/was solely for non-production validation of the default namespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the production check to just before the error throw, hopefully that sorts things.

} else {
throw new Error(`Default namespace not found at ${defaultNSPath}`)
}
}
}

// Set server side backend
combinedConfig.backend = {
loadPath: path.join(process.cwd(), `${localePath}/${localeStructure}.${localeExtension}`),
addPath: path.join(process.cwd(), `${localePath}/${localeStructure}.missing.${localeExtension}`),
loadPath: path.join(process.cwd(), `${serverLocalePath}/${localeStructure}.${localeExtension}`),
addPath: path.join(process.cwd(), `${serverLocalePath}/${localeStructure}.missing.${localeExtension}`),
}

// Set server side preload (languages and namespaces)
combinedConfig.preload = allLanguages
if (!combinedConfig.ns) {
const getAllNamespaces = p => fs.readdirSync(p).map(file => file.replace(`.${localeExtension}`, ''))
combinedConfig.ns = getAllNamespaces(path.join(process.cwd(), `${localePath}/${defaultLanguage}`))
combinedConfig.ns = getAllNamespaces(path.join(process.cwd(), `${serverLocalePath}/${defaultLanguage}`))
}

} else {

let clientLocalePath = localePath
// remove public/ prefix from client site config
if (localePath.startsWith('public/')) {
clientLocalePath = localePath.replace(/^public\//, '')
}

// Set client side backend
combinedConfig.backend = {
loadPath: `/${localePath}/${localeStructure}.${localeExtension}`,
addPath: `/${localePath}/${localeStructure}.missing.${localeExtension}`,
loadPath: `/${clientLocalePath}/${localeStructure}.${localeExtension}`,
addPath: `/${clientLocalePath}/${localeStructure}.missing.${localeExtension}`,
}

combinedConfig.ns = [combinedConfig.defaultNS]
Expand Down
4 changes: 2 additions & 2 deletions src/config/default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isServer } from '../utils'
const DEFAULT_LANGUAGE = 'en'
const OTHER_LANGUAGES = []
const DEFAULT_NAMESPACE = 'common'
const LOCALE_PATH = 'static/locales'
const LOCALE_PATH = 'public/static/locales'
const LOCALE_STRUCTURE = '{{lng}}/{{ns}}'
const LOCALE_EXTENSION = 'json'

Expand All @@ -24,7 +24,7 @@ export const defaultConfig = {
},
browserLanguageDetection: true,
serverLanguageDetection: true,
ignoreRoutes: ['/_next/', '/static/'],
ignoreRoutes: ['/_next/', '/static/', '/public/'],
customDetectors: [],
detection: {
lookupCookie: 'next-i18next',
Expand Down