ghost 4.18.0 → 4.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/.eslintrc.js +9 -8
  2. package/Gruntfile.js +1 -1
  3. package/PRIVACY.md +3 -0
  4. package/content/adapters/README.md +2 -2
  5. package/core/boot.js +17 -12
  6. package/core/bridge.js +9 -1
  7. package/core/built/assets/{chunk.3.b80d3e1e6b8556aaff3c.js → chunk.3.777d43e2ce954ba8b2f5.js} +25 -25
  8. package/core/built/assets/codemirror/{codemirror-21a09582262987037db73b152fb35f7c.js → codemirror-d25c379b87ec8b33d54ac7149bc0b6ae.js} +14 -14
  9. package/core/built/assets/ghost-dark-20e2892d4f30d0d1183c9ac725ea37d0.css +1 -0
  10. package/core/built/assets/{ghost.min-88d647a008a5b1dd678a89ae1e55c038.js → ghost.min-26e427944e719b616b8dc7fbb3bbd2f9.js} +709 -422
  11. package/core/built/assets/ghost.min-57e46fd3b1145ecf2cbd185a13611f3b.css +1 -0
  12. package/core/built/assets/icons/arrow-left-small.svg +0 -4
  13. package/core/built/assets/icons/paintbrush.svg +10 -1
  14. package/core/built/assets/icons/post.svg +3 -1
  15. package/core/built/assets/img/footer-marketplace-bg-572b6c6486a7e26316954d599eaa9f30.png +0 -0
  16. package/core/built/assets/img/marketing/offers-1-f2e1b653c4d5bb90eea9d7a2862530f9.jpg +0 -0
  17. package/core/built/assets/img/marketing/offers-2-28a225d34cc39d133748431536961d00.jpg +0 -0
  18. package/core/built/assets/img/marketing/offers-3-2094c91ab21a16c37fbe6ec16c140160.jpg +0 -0
  19. package/core/built/assets/img/themes/Alto-f4db5af43ca9771c7ac1f754de3ddf2f.png +0 -0
  20. package/core/built/assets/img/themes/Bulletin-57d45b992ff0e26e0acdce7ed4cccd67.png +0 -0
  21. package/core/built/assets/img/themes/Casper-c7e784d7188cc5d7f097d9b6c97b0263.jpg +0 -0
  22. package/core/built/assets/img/themes/Dawn-be81aa8c8caae8fcfb5d5fbec823fdcc.png +0 -0
  23. package/core/built/assets/img/themes/Digest-d3467ac22a290e1ad3a543014758286e.png +0 -0
  24. package/core/built/assets/img/themes/Dope-6f8e0bbc199ce4af9a60859e9e6a74ad.png +0 -0
  25. package/core/built/assets/img/themes/Ease-9c279ea6cec3c0f1823f81c9dd24b116.png +0 -0
  26. package/core/built/assets/img/themes/Edge-0258906309e11fd075a1d9880aa09b20.png +0 -0
  27. package/core/built/assets/img/themes/Edition-d8f508e93bc24bdf2716ae6f8b3d44f8.png +0 -0
  28. package/core/built/assets/img/themes/Editorial-a25a4a34c04dedd858bd5e05ef388b1c.jpg +0 -0
  29. package/core/built/assets/img/themes/Journal-accf0031bbae0919900a049061e65a04.png +0 -0
  30. package/core/built/assets/img/themes/London-3f07efcee9e5bfb9a33827064eb77e70.jpg +0 -0
  31. package/core/built/assets/img/themes/Massively-06edf00108429f7fb8e65f190fba34fe.jpg +0 -0
  32. package/core/built/assets/img/themes/Ruby-11a53c62015612f4b3aca8f503121225.png +0 -0
  33. package/core/built/assets/img/themes/Wave-86e8044c2d76cb57a9030e4c24ac9520.png +0 -0
  34. package/core/built/assets/simplemde/{simplemde-232f69d126310434489071a1891e6d8b.js → simplemde-3ffc0ec9e9fecf29b9a499db678c9e65.js} +14 -14
  35. package/core/built/assets/{vendor.min-7dc7cf9c92175ebfb9cea95c120ee8a7.js → vendor.min-af502ac4142871500fc424f6a5a254ec.js} +2206 -1859
  36. package/core/frontend/apps/amp/lib/router.js +1 -1
  37. package/core/frontend/helpers/match.js +17 -23
  38. package/core/frontend/meta/author-url.js +1 -1
  39. package/core/frontend/meta/url.js +1 -1
  40. package/core/{server → frontend}/public/favicon.ico +0 -0
  41. package/core/{server → frontend}/public/ghost.css +0 -0
  42. package/core/{server → frontend}/public/ghost.min.css +0 -0
  43. package/core/{server → frontend}/public/robots.txt +0 -0
  44. package/core/{server → frontend}/public/sitemap.xsl +0 -0
  45. package/core/frontend/services/proxy.js +1 -1
  46. package/core/frontend/services/rendering.js +1 -1
  47. package/core/frontend/services/routing/CollectionRouter.js +3 -49
  48. package/core/frontend/services/routing/ParentRouter.js +1 -4
  49. package/core/frontend/services/routing/StaticPagesRouter.js +3 -5
  50. package/core/frontend/services/routing/StaticRoutesRouter.js +4 -6
  51. package/core/frontend/services/routing/TaxonomyRouter.js +4 -5
  52. package/core/frontend/services/routing/controllers/collection.js +2 -2
  53. package/core/frontend/services/routing/controllers/email-post.js +2 -2
  54. package/core/frontend/services/routing/controllers/entry.js +2 -2
  55. package/core/frontend/services/routing/controllers/preview.js +2 -2
  56. package/core/frontend/services/routing/index.js +6 -12
  57. package/core/frontend/services/routing/registry.js +13 -0
  58. package/core/frontend/services/routing/router-manager.js +185 -0
  59. package/core/frontend/services/rss/generate-feed.js +2 -2
  60. package/core/frontend/services/theme-engine/i18n/i18n.js +267 -28
  61. package/core/frontend/services/theme-engine/i18n/index.js +1 -1
  62. package/core/frontend/services/theme-engine/i18n/theme-i18n.js +73 -0
  63. package/core/frontend/web/index.js +1 -0
  64. package/core/{server/web/site → frontend/web}/middleware/handle-image-sizes.js +4 -4
  65. package/core/{server/web/site → frontend/web}/middleware/index.js +0 -0
  66. package/core/{server/web/site → frontend/web}/middleware/redirect-ghost-to-admin.js +3 -3
  67. package/core/{server/web/site → frontend/web}/middleware/serve-favicon.js +6 -6
  68. package/core/{server/web/site → frontend/web}/middleware/serve-public-file.js +2 -2
  69. package/core/{server/web/site → frontend/web}/middleware/static-theme.js +3 -3
  70. package/core/frontend/web/routes.js +13 -0
  71. package/core/{server/web/site/app.js → frontend/web/site.js} +12 -16
  72. package/core/server/adapters/storage/LocalFileStorage.js +35 -39
  73. package/core/server/adapters/storage/index.js +12 -2
  74. package/core/server/api/canary/custom-theme-settings.js +2 -2
  75. package/core/server/api/canary/images.js +1 -1
  76. package/core/server/api/canary/oembed.js +2 -2
  77. package/core/server/api/canary/offers.js +29 -1
  78. package/core/server/api/canary/posts-public.js +6 -2
  79. package/core/server/api/canary/products.js +6 -2
  80. package/core/server/api/canary/tags-public.js +6 -2
  81. package/core/server/api/canary/users.js +9 -4
  82. package/core/server/api/canary/utils/serializers/output/custom-theme-settings.js +2 -2
  83. package/core/server/api/canary/utils/serializers/output/notifications.js +1 -0
  84. package/core/server/api/canary/utils/serializers/output/settings.js +2 -3
  85. package/core/server/api/canary/utils/serializers/output/utils/url.js +1 -1
  86. package/core/server/api/canary/utils/validators/input/oembed.js +4 -1
  87. package/core/server/api/canary/utils/validators/input/passwordreset.js +8 -3
  88. package/core/server/api/canary/utils/validators/input/settings.js +5 -4
  89. package/core/server/api/canary/utils/validators/input/setup.js +6 -2
  90. package/core/server/api/canary/utils/validators/input/users.js +6 -2
  91. package/core/server/api/canary/utils/validators/input/webhooks.js +8 -3
  92. package/core/server/api/v2/images.js +1 -1
  93. package/core/server/api/v2/utils/serializers/output/authentication.js +9 -4
  94. package/core/server/api/v2/utils/serializers/output/notifications.js +1 -0
  95. package/core/server/api/v2/utils/serializers/output/users.js +5 -3
  96. package/core/server/api/v2/utils/serializers/output/utils/url.js +1 -1
  97. package/core/server/api/v2/utils/validators/input/images.js +11 -6
  98. package/core/server/api/v2/utils/validators/input/invitations.js +14 -6
  99. package/core/server/api/v2/utils/validators/input/invites.js +6 -2
  100. package/core/server/api/v2/utils/validators/input/oembed.js +6 -2
  101. package/core/server/api/v2/utils/validators/input/passwordreset.js +8 -3
  102. package/core/server/api/v2/utils/validators/input/settings.js +10 -4
  103. package/core/server/api/v2/utils/validators/input/setup.js +6 -2
  104. package/core/server/api/v2/utils/validators/input/users.js +5 -2
  105. package/core/server/api/v3/authentication.js +6 -2
  106. package/core/server/api/v3/authors-public.js +6 -2
  107. package/core/server/api/v3/email.js +9 -4
  108. package/core/server/api/v3/images.js +1 -1
  109. package/core/server/api/v3/integrations.js +7 -3
  110. package/core/server/api/v3/invites.js +6 -3
  111. package/core/server/api/v3/labels.js +10 -5
  112. package/core/server/api/v3/memberSigninUrls.js +5 -2
  113. package/core/server/api/v3/oembed.js +2 -2
  114. package/core/server/api/v3/pages-public.js +5 -2
  115. package/core/server/api/v3/pages.js +6 -3
  116. package/core/server/api/v3/posts-public.js +5 -3
  117. package/core/server/api/v3/posts.js +7 -3
  118. package/core/server/api/v3/preview.js +5 -3
  119. package/core/server/api/v3/session.js +7 -3
  120. package/core/server/api/v3/settings.js +8 -3
  121. package/core/server/api/v3/slugs.js +5 -4
  122. package/core/server/api/v3/utils/serializers/output/authentication.js +10 -4
  123. package/core/server/api/v3/utils/serializers/output/notifications.js +1 -0
  124. package/core/server/api/v3/utils/serializers/output/settings.js +2 -3
  125. package/core/server/api/v3/utils/serializers/output/users.js +6 -2
  126. package/core/server/api/v3/utils/serializers/output/utils/url.js +1 -1
  127. package/core/server/api/v3/utils/validators/input/images.js +12 -7
  128. package/core/server/api/v3/utils/validators/input/invitations.js +14 -6
  129. package/core/server/api/v3/utils/validators/input/invites.js +6 -2
  130. package/core/server/api/v3/utils/validators/input/oembed.js +6 -2
  131. package/core/server/api/v3/utils/validators/input/passwordreset.js +8 -3
  132. package/core/server/api/v3/utils/validators/input/settings.js +5 -4
  133. package/core/server/api/v3/utils/validators/input/setup.js +6 -2
  134. package/core/server/api/v3/utils/validators/input/users.js +6 -2
  135. package/core/server/api/v3/utils/validators/input/webhooks.js +8 -3
  136. package/core/server/data/exporter/table-lists.js +2 -1
  137. package/core/server/data/importer/handlers/image.js +1 -1
  138. package/core/server/data/importer/importers/image.js +1 -1
  139. package/core/server/data/migrations/init/1-create-tables.js +7 -8
  140. package/core/server/data/migrations/init/2-create-fixtures.js +8 -8
  141. package/core/server/data/migrations/versions/4.19/01-add-active-column-to-offers.js +7 -0
  142. package/core/server/data/migrations/versions/4.19/02-add-offer-redemptions-table.js +8 -0
  143. package/core/server/data/migrations/versions/4.20/01-remove-offer-redemptions-table.js +19 -0
  144. package/core/server/data/migrations/versions/4.20/02-remove-offers-table.js +30 -0
  145. package/core/server/data/migrations/versions/4.20/03-add-offers-table.js +21 -0
  146. package/core/server/data/migrations/versions/4.20/04-add-offer-redemptions-table.js +9 -0
  147. package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +41 -0
  148. package/core/server/data/schema/fixtures/utils.js +150 -143
  149. package/core/server/data/schema/schema.js +15 -3
  150. package/core/server/frontend/ghost.min.css +1 -0
  151. package/core/server/lib/image/blog-icon.js +10 -10
  152. package/core/server/lib/image/image-size.js +5 -5
  153. package/core/server/lib/image/image-utils.js +4 -4
  154. package/core/server/lib/image/index.js +1 -2
  155. package/core/server/lib/mobiledoc.js +3 -2
  156. package/core/server/models/action.js +7 -4
  157. package/core/server/models/base/plugins/overrides.js +19 -6
  158. package/core/server/models/custom-theme-setting.js +56 -1
  159. package/core/server/models/index.js +4 -45
  160. package/core/server/models/member.js +5 -0
  161. package/core/server/models/offer-redemption.js +10 -0
  162. package/core/server/models/user.js +2 -1
  163. package/core/server/overrides.js +6 -2
  164. package/core/server/run-update-check.js +0 -3
  165. package/core/server/services/adapter-manager/config.js +1 -0
  166. package/core/server/services/adapter-manager/index.js +9 -5
  167. package/core/server/services/adapter-manager/options-resolver.js +18 -0
  168. package/core/server/services/bulk-email/bulk-email-processor.js +6 -2
  169. package/core/server/services/bulk-email/mailgun.js +1 -1
  170. package/core/server/services/custom-theme-settings.js +10 -4
  171. package/core/server/services/invites/index.js +0 -2
  172. package/core/server/services/invites/invites.js +5 -5
  173. package/core/server/services/mail/GhostMailer.js +18 -10
  174. package/core/server/services/mega/mega.js +3 -3
  175. package/core/server/services/mega/post-email-serializer.js +2 -2
  176. package/core/server/services/members/api.js +3 -4
  177. package/core/server/services/members/emails/signin.js +1 -1
  178. package/core/server/services/members/emails/signup.js +1 -1
  179. package/core/server/services/members/emails/subscribe.js +1 -1
  180. package/core/server/services/members/middleware.js +10 -0
  181. package/core/server/services/members/service.js +2 -1
  182. package/core/server/services/notifications/index.js +1 -1
  183. package/core/server/services/notifications/notifications.js +40 -35
  184. package/core/server/services/oembed.js +4 -9
  185. package/core/server/services/offers/service.js +16 -6
  186. package/core/server/services/permissions/public.js +6 -2
  187. package/core/server/services/route-settings/route-settings.js +1 -1
  188. package/core/server/services/settings/index.js +3 -1
  189. package/core/server/services/settings/settings-bread-service.js +42 -20
  190. package/core/server/services/slack.js +1 -1
  191. package/core/server/services/themes/activate.js +2 -2
  192. package/core/server/services/themes/activation-bridge.js +6 -6
  193. package/core/server/services/themes/storage.js +1 -1
  194. package/core/{frontend → server}/services/url/Queue.js +0 -0
  195. package/core/{frontend → server}/services/url/Resource.js +0 -0
  196. package/core/{frontend → server}/services/url/Resources.js +2 -2
  197. package/core/{frontend → server}/services/url/UrlGenerator.js +14 -14
  198. package/core/{frontend → server}/services/url/UrlService.js +12 -15
  199. package/core/{frontend → server}/services/url/Urls.js +1 -1
  200. package/core/{frontend → server}/services/url/configs/canary.js +0 -0
  201. package/core/{frontend → server}/services/url/configs/v2.js +0 -0
  202. package/core/{frontend → server}/services/url/configs/v3.js +0 -0
  203. package/core/{frontend → server}/services/url/configs/v4.js +0 -0
  204. package/core/{frontend → server}/services/url/index.js +0 -0
  205. package/core/server/services/xmlrpc.js +1 -1
  206. package/core/server/update-check.js +3 -3
  207. package/core/server/web/admin/controller.js +11 -0
  208. package/core/server/web/admin/views/default-prod.html +4 -4
  209. package/core/server/web/admin/views/default.html +4 -4
  210. package/core/server/web/api/app.js +8 -9
  211. package/core/server/web/members/app.js +1 -0
  212. package/core/server/web/oauth/app.js +4 -2
  213. package/core/server/web/parent/backend.js +3 -3
  214. package/core/server/web/parent/frontend.js +2 -2
  215. package/core/server/web/shared/middlewares/api/spam-prevention.js +0 -2
  216. package/core/server/web/shared/middlewares/custom-redirects.js +0 -8
  217. package/core/server/web/shared/middlewares/maintenance.js +1 -1
  218. package/core/server/web/well-known.js +10 -10
  219. package/core/shared/config/defaults.json +2 -2
  220. package/core/shared/config/overrides.json +1 -1
  221. package/core/shared/express.js +10 -0
  222. package/core/shared/html-to-plaintext.js +2 -2
  223. package/core/shared/labs.js +14 -6
  224. package/loggingrc.js +10 -0
  225. package/package.json +60 -57
  226. package/yarn.lock +1317 -914
  227. package/core/built/assets/ghost-dark-13627f10941a7dbb2b12e1d41dc51c34.css +0 -1
  228. package/core/built/assets/ghost.min-d9cbfb4eb2db8915fcd2bf2416218616.css +0 -1
  229. package/core/built/assets/img/themes/London-68501c8ab797de7f2851cf9ea0a28e26.jpg +0 -0
  230. package/core/frontend/services/routing/bootstrap.js +0 -114
  231. package/core/server/public/404-ghost.png +0 -0
  232. package/core/server/public/404-ghost@2x.png +0 -0
  233. package/core/server/web/site/index.js +0 -1
  234. package/core/server/web/site/routes.js +0 -9
  235. package/core/shared/i18n/i18n.js +0 -312
  236. package/core/shared/i18n/index.js +0 -6
  237. package/core/shared/i18n/translations/en.json +0 -675
@@ -1,114 +0,0 @@
1
- const debug = require('@tryghost/debug')('routing');
2
- const _ = require('lodash');
3
- const StaticRoutesRouter = require('./StaticRoutesRouter');
4
- const StaticPagesRouter = require('./StaticPagesRouter');
5
- const CollectionRouter = require('./CollectionRouter');
6
- const TaxonomyRouter = require('./TaxonomyRouter');
7
- const PreviewRouter = require('./PreviewRouter');
8
- const ParentRouter = require('./ParentRouter');
9
- const EmailRouter = require('./EmailRouter');
10
- const UnsubscribeRouter = require('./UnsubscribeRouter');
11
-
12
- // This emits its own routing events
13
- const events = require('../../../server/lib/common/events');
14
-
15
- const defaultApiVersion = 'v4';
16
-
17
- const registry = require('./registry');
18
- let siteRouter;
19
-
20
- /**
21
- * @description The `init` function will return the wrapped parent express router and will start creating all
22
- * routers if you pass the option "start: true".
23
- *
24
- * CASES:
25
- * - if Ghost starts, it will first init the site app with the wrapper router and then call `start`
26
- * separately, because it could be that your blog goes into maintenance mode
27
- * - if you change your route settings, we will re-initialise routing
28
- *
29
- * @param {Object} options
30
- * @returns {ExpressRouter}
31
- */
32
- module.exports.init = ({start = false, routerSettings, apiVersion}) => {
33
- debug('bootstrap init', start, apiVersion, routerSettings);
34
-
35
- registry.resetAllRouters();
36
- registry.resetAllRoutes();
37
-
38
- events.emit('routers.reset');
39
-
40
- siteRouter = new ParentRouter('SiteRouter');
41
- registry.setRouter('siteRouter', siteRouter);
42
-
43
- if (start) {
44
- apiVersion = apiVersion || defaultApiVersion;
45
- this.start(apiVersion, routerSettings);
46
- }
47
-
48
- return siteRouter.router();
49
- };
50
-
51
- /**
52
- * @description This function will create the routers based on the route settings
53
- *
54
- * The routers are created in a specific order. This order defines who can get a resource first or
55
- * who can dominant other routers.
56
- *
57
- * 1. Preview + Unsubscribe Routers: Strongest inbuilt features, which you can never override.
58
- * 2. Static Routes: Very strong, because you can override any urls and redirect to a static route.
59
- * 3. Taxonomies: Stronger than collections, because it's an inbuilt feature.
60
- * 4. Collections
61
- * 5. Static Pages: Weaker than collections, because we first try to find a post slug and fallback to lookup a static page.
62
- * 6. Internal Apps: Weakest
63
- *
64
- * @param {string} apiVersion
65
- * @param {object} routerSettings
66
- */
67
- module.exports.start = (apiVersion, routerSettings) => {
68
- debug('bootstrap start', apiVersion, routerSettings);
69
- const RESOURCE_CONFIG = require(`./config/${apiVersion}`);
70
-
71
- const unsubscribeRouter = new UnsubscribeRouter();
72
- siteRouter.mountRouter(unsubscribeRouter.router());
73
- registry.setRouter('unsubscribeRouter', unsubscribeRouter);
74
-
75
- const emailRouter = new EmailRouter(RESOURCE_CONFIG);
76
- siteRouter.mountRouter(emailRouter.router());
77
- registry.setRouter('emailRouter', emailRouter);
78
-
79
- const previewRouter = new PreviewRouter(RESOURCE_CONFIG);
80
- siteRouter.mountRouter(previewRouter.router());
81
- registry.setRouter('previewRouter', previewRouter);
82
-
83
- _.each(routerSettings.routes, (value, key) => {
84
- const staticRoutesRouter = new StaticRoutesRouter(key, value);
85
- siteRouter.mountRouter(staticRoutesRouter.router());
86
-
87
- registry.setRouter(staticRoutesRouter.identifier, staticRoutesRouter);
88
- });
89
-
90
- _.each(routerSettings.collections, (value, key) => {
91
- const collectionRouter = new CollectionRouter(key, value, RESOURCE_CONFIG);
92
- siteRouter.mountRouter(collectionRouter.router());
93
- registry.setRouter(collectionRouter.identifier, collectionRouter);
94
- });
95
-
96
- const staticPagesRouter = new StaticPagesRouter(RESOURCE_CONFIG);
97
- siteRouter.mountRouter(staticPagesRouter.router());
98
-
99
- registry.setRouter('staticPagesRouter', staticPagesRouter);
100
-
101
- _.each(routerSettings.taxonomies, (value, key) => {
102
- const taxonomyRouter = new TaxonomyRouter(key, value, RESOURCE_CONFIG);
103
- siteRouter.mountRouter(taxonomyRouter.router());
104
-
105
- registry.setRouter(taxonomyRouter.identifier, taxonomyRouter);
106
- });
107
-
108
- const appRouter = new ParentRouter('AppsRouter');
109
- siteRouter.mountRouter(appRouter.router());
110
-
111
- registry.setRouter('appRouter', appRouter);
112
-
113
- debug('Routes:', registry.getAllRoutes());
114
- };
Binary file
Binary file
@@ -1 +0,0 @@
1
- module.exports = require('./app');
@@ -1,9 +0,0 @@
1
- const debug = require('@tryghost/debug')('routing');
2
- const routing = require('../../../frontend/services/routing');
3
- const routeSettings = require('../../services/route-settings');
4
-
5
- module.exports = function siteRoutes(options = {}) {
6
- debug('site Routes', options);
7
- options.routerSettings = routeSettings.loadRouteSettingsSync();
8
- return routing.bootstrap.init(options);
9
- };
@@ -1,312 +0,0 @@
1
- const errors = require('@tryghost/errors');
2
- const fs = require('fs-extra');
3
- const path = require('path');
4
- const MessageFormat = require('intl-messageformat');
5
- const jp = require('jsonpath');
6
- const isString = require('lodash/isString');
7
- const isObject = require('lodash/isObject');
8
- const isEqual = require('lodash/isEqual');
9
- const isNil = require('lodash/isNil');
10
- const merge = require('lodash/merge');
11
- const get = require('lodash/get');
12
-
13
- class I18n {
14
- /**
15
- * @param {objec} [options]
16
- * @param {string} basePath - the base path to the translations directory
17
- * @param {string} [locale] - a locale string
18
- * @param {{dot|fulltext}} [stringMode] - which mode our translation keys use
19
- * @param {{object}} [logging] - logging method
20
- */
21
- constructor(options = {}) {
22
- this._basePath = options.basePath || __dirname;
23
- this._locale = options.locale || this.defaultLocale();
24
- this._stringMode = options.stringMode || 'dot';
25
- this._logging = options.logging || console;
26
-
27
- this._strings = null;
28
- }
29
-
30
- /**
31
- * BasePath getter & setter used for testing
32
- */
33
- set basePath(basePath) {
34
- this._basePath = basePath;
35
- }
36
-
37
- /**
38
- * Need to call init after this
39
- */
40
- get basePath() {
41
- return this._basePath;
42
- }
43
-
44
- /**
45
- * English is our default locale
46
- */
47
- defaultLocale() {
48
- return 'en';
49
- }
50
-
51
- supportedLocales() {
52
- return [this.defaultLocale()];
53
- }
54
-
55
- /**
56
- * Exporting the current locale (e.g. "en") to make it available for other files as well,
57
- * such as core/frontend/helpers/date.js and core/frontend/helpers/lang.js
58
- */
59
- locale() {
60
- return this._locale;
61
- }
62
-
63
- /**
64
- * Helper method to find and compile the given data context with a proper string resource.
65
- *
66
- * @param {string} translationPath Path within the JSON language file to desired string (ie: "errors.init.jsNotBuilt")
67
- * @param {object} [bindings]
68
- * @returns {string}
69
- */
70
- t(translationPath, bindings) {
71
- let string;
72
- let msg;
73
-
74
- string = this._findString(translationPath);
75
-
76
- // If the path returns an array (as in the case with anything that has multiple paragraphs such as emails), then
77
- // loop through them and return an array of translated/formatted strings. Otherwise, just return the normal
78
- // translated/formatted string.
79
- if (Array.isArray(string)) {
80
- msg = [];
81
- string.forEach(function (s) {
82
- msg.push(this._formatMessage(s, bindings));
83
- });
84
- } else {
85
- msg = this._formatMessage(string, bindings);
86
- }
87
-
88
- return msg;
89
- }
90
-
91
- /**
92
- * Setup i18n support:
93
- * - Load proper language file into memory
94
- */
95
- init() {
96
- this._strings = this._loadStrings();
97
-
98
- this._initializeIntl();
99
- }
100
-
101
- /**
102
- * Attempt to load strings from a file
103
- *
104
- * @param {sting} [locale]
105
- * @returns {object} strings
106
- */
107
- _loadStrings(locale) {
108
- locale = locale || this.locale();
109
-
110
- try {
111
- return this._readTranslationsFile(locale);
112
- } catch (err) {
113
- if (err.code === 'ENOENT') {
114
- this._handleMissingFileError(locale, err);
115
-
116
- if (locale !== this.defaultLocale()) {
117
- this._handleFallbackToDefault();
118
- return this._loadStrings(this.defaultLocale());
119
- }
120
- } else if (err instanceof SyntaxError) {
121
- this._handleInvalidFileError(locale, err);
122
- } else {
123
- throw err;
124
- }
125
-
126
- // At this point we've done all we can and strings must be an object
127
- return {};
128
- }
129
- }
130
-
131
- /**
132
- * Do the lookup within the JSON file using jsonpath
133
- *
134
- * @param {String} msgPath
135
- */
136
- _getCandidateString(msgPath) {
137
- // Our default string mode is "dot" for dot-notation, e.g. $.something.like.this used in the backend
138
- // Both jsonpath's dot-notation and bracket-notation start with '$' E.g.: $.store.book.title or $['store']['book']['title']
139
- // While bracket-notation allows any Unicode characters in keys (i.e. for themes / fulltext mode) E.g. $['Read more']
140
- // dot-notation allows only word characters in keys for backend messages (that is \w or [A-Za-z0-9_] in RegExp)
141
- let jsonPath = `$.${msgPath}`;
142
- let fallback = null;
143
-
144
- if (this._stringMode === 'fulltext') {
145
- jsonPath = jp.stringify(['$', msgPath]);
146
- // In fulltext mode we can use the passed string as a fallback
147
- fallback = msgPath;
148
- }
149
-
150
- try {
151
- return jp.value(this._strings, jsonPath) || fallback;
152
- } catch (err) {
153
- this._handleInvalidKeyError(msgPath, err);
154
- }
155
- }
156
-
157
- /**
158
- * Parse JSON file for matching locale, returns string giving path.
159
- *
160
- * @param {string} msgPath Path with in the JSON language file to desired string (ie: "errors.init.jsNotBuilt")
161
- * @returns {string}
162
- */
163
- _findString(msgPath, opts) {
164
- const options = merge({log: true}, opts || {});
165
- let candidateString;
166
- let matchingString;
167
-
168
- // no path? no string
169
- if (msgPath.length === 0 || !isString(msgPath)) {
170
- this._handleEmptyKeyError();
171
- return '';
172
- }
173
-
174
- // If not in memory, load translations for core
175
- if (isNil(this._strings)) {
176
- this._handleUninitialisedError(msgPath);
177
- }
178
-
179
- candidateString = this._getCandidateString(msgPath);
180
-
181
- matchingString = candidateString || {};
182
-
183
- if (isObject(matchingString) || isEqual(matchingString, {})) {
184
- if (options.log) {
185
- this._handleMissingKeyError(msgPath);
186
- }
187
-
188
- matchingString = this._fallbackError();
189
- }
190
-
191
- return matchingString;
192
- }
193
-
194
- _translationFileDirs() {
195
- return [this.basePath];
196
- }
197
-
198
- // If we are passed a locale, use that, else use this.locale
199
- _translationFileName(locale) {
200
- return `${locale || this.locale()}.json`;
201
- }
202
-
203
- /**
204
- * Read the translations file
205
- * Error handling to be done by consumer
206
- *
207
- * @param {string} locale
208
- */
209
- _readTranslationsFile(locale) {
210
- const filePath = path.join(...this._translationFileDirs(), this._translationFileName(locale));
211
- const content = fs.readFileSync(filePath);
212
- return JSON.parse(content);
213
- }
214
-
215
- /**
216
- * Format the string using the correct locale and applying any bindings
217
- * @param {String} string
218
- * @param {Object} bindings
219
- */
220
- _formatMessage(string, bindings) {
221
- let currentLocale = this.locale();
222
- let msg = new MessageFormat(string, currentLocale);
223
-
224
- try {
225
- msg = msg.format(bindings);
226
- } catch (err) {
227
- this._handleFormatError(err);
228
-
229
- // fallback
230
- msg = new MessageFormat(this._fallbackError(), currentLocale);
231
- msg = msg.format();
232
- }
233
-
234
- return msg;
235
- }
236
-
237
- /**
238
- * [Private] Setup i18n support:
239
- * - Polyfill node.js if it does not have Intl support or support for a particular locale
240
- */
241
- _initializeIntl() {
242
- let hasBuiltInLocaleData;
243
- let IntlPolyfill;
244
-
245
- if (global.Intl) {
246
- // Determine if the built-in `Intl` has the locale data we need.
247
- hasBuiltInLocaleData = this.supportedLocales().every(function (locale) {
248
- return Intl.NumberFormat.supportedLocalesOf(locale)[0] === locale &&
249
- Intl.DateTimeFormat.supportedLocalesOf(locale)[0] === locale;
250
- });
251
- if (!hasBuiltInLocaleData) {
252
- // `Intl` exists, but it doesn't have the data we need, so load the
253
- // polyfill and replace the constructors with need with the polyfill's.
254
- IntlPolyfill = require('intl');
255
- Intl.NumberFormat = IntlPolyfill.NumberFormat;
256
- Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
257
- }
258
- } else {
259
- // No `Intl`, so use and load the polyfill.
260
- global.Intl = require('intl');
261
- }
262
- }
263
-
264
- _handleUninitialisedError(key) {
265
- this._logging.warn(`i18n was used before it was initialised with key ${key}`);
266
- this.init();
267
- }
268
-
269
- _handleFormatError(err) {
270
- this._logging.error(err.message);
271
- }
272
-
273
- _handleFallbackToDefault() {
274
- this._logging.warn(`i18n is falling back to ${this.defaultLocale()}.json.`);
275
- }
276
-
277
- _handleMissingFileError(locale) {
278
- this._logging.warn(`i18n was unable to find ${locale}.json.`);
279
- }
280
- _handleInvalidFileError(locale, err) {
281
- this._logging.error(new errors.IncorrectUsageError({
282
- err,
283
- message: `i18n was unable to parse ${locale}.json. Please check that it is valid JSON.`
284
- }));
285
- }
286
-
287
- _handleEmptyKeyError() {
288
- this._logging.warn('i18n.t() was called without a key');
289
- }
290
-
291
- _handleMissingKeyError(key) {
292
- this._logging.error(new errors.IncorrectUsageError({
293
- message: `i18n.t() was called with a key that could not be found: ${key}`
294
- }));
295
- }
296
-
297
- _handleInvalidKeyError(key, err) {
298
- throw new errors.IncorrectUsageError({
299
- err,
300
- message: `i18n.t() called with an invalid key: ${key}`
301
- });
302
- }
303
-
304
- /**
305
- * A really basic error for if everything goes wrong
306
- */
307
- _fallbackError() {
308
- return get(this._strings, 'errors.errors.anErrorOccurred', 'An error occurred');
309
- }
310
- }
311
-
312
- module.exports = I18n;
@@ -1,6 +0,0 @@
1
- const path = require('path');
2
- const logging = require('@tryghost/logging');
3
- const I18n = require('./i18n');
4
-
5
- module.exports = new I18n({logging, basePath: path.join(__dirname, 'translations')});
6
- module.exports.I18n = I18n;