├── .env.sample ├── .eslintignore ├── .eslintrc.json ├── .github ├── labeler.yml └── workflows │ ├── label.yml │ └── units.yml ├── .gitignore ├── .nvmrc ├── _config ├── billingOptions.ts ├── config.ts └── installInitialDataMongo.ts ├── _constants ├── colorSets.ts └── index.ts ├── _gql ├── createBillingSubscription.ts └── getActiveSubscription.ts ├── _middleware ├── verifiedConnection.ts └── verifiedWebhook.ts ├── _store ├── initialState.ts ├── reducers │ ├── app.ts │ ├── loading.ts │ └── shop.tsx ├── rootReducer.ts └── store.ts ├── _types ├── safelyGetNestedText.d.ts ├── shouldFetchTranslations.d.ts ├── textProvider.d.ts └── verifiedConnections.d.ts ├── _utils ├── arrayContainsArray.ts ├── atlasMethods.ts ├── buildAuthUrl.ts ├── buildGqlEndpoint.ts ├── buildGqlHeaders.ts ├── buildHeaders.ts ├── buildInstallUrl.ts ├── dataShapers │ ├── dataShapeBilling.ts │ ├── dataShapeBillingVerify.ts │ ├── dataShapeCustomers.ts │ ├── dataShapeOders.ts │ └── dataShapeProducts.ts ├── safelyGetNestedText.ts ├── shopifyMethods.ts ├── shouldFetchtranslation.ts └── validateWebhook.ts ├── components ├── AppBridgeProvider.tsx ├── CustomLink.tsx ├── I18nProvider.tsx ├── Navigation │ └── TopNav.tsx ├── PolarisProvider.tsx ├── Stage.tsx ├── TextProvider.tsx ├── billing │ ├── BillingBannerInit.tsx │ ├── BillingCards.tsx │ ├── BillingFeatureList.tsx │ └── BillingSelector.tsx └── global │ ├── LoadingBar.tsx │ └── LoadingPage.tsx ├── hooks ├── useAppBridge.tsx ├── useBilling.tsx ├── useMutation.tsx ├── useQuery.tsx ├── useRouterSync.tsx ├── useShopData.tsx └── useShopDomain.tsx ├── jest.config.js ├── next-env.d.ts ├── next.config.js ├── now.json ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── api │ ├── auth.ts │ ├── install.ts │ ├── query.ts │ ├── verifybilling.ts │ └── webhooks │ │ ├── customers │ │ ├── data_request.ts │ │ └── redact.ts │ │ └── shop │ │ └── redact.ts ├── billing.tsx ├── dashboard.tsx ├── index.tsx └── settings.tsx ├── public ├── favicon.ico ├── locales │ ├── cs.json │ ├── da.json │ ├── de.json │ ├── en.json │ ├── es.json │ ├── fi.json │ ├── fr.json │ ├── hi.json │ ├── it.json │ ├── ja.json │ ├── ko.json │ ├── ms.json │ ├── nb.json │ ├── nl.json │ ├── pl.json │ ├── pt-BR.json │ ├── pt-PT.json │ ├── sv.json │ ├── th.json │ ├── tr.json │ ├── zh-CN.json │ └── zh-TW.json ├── triangle-of-triangles.png └── triangle.svg ├── pull_request_template.md ├── readme.md ├── tests └── utils │ ├── arrayContainsArray.unit.test.ts │ ├── buildAuthUrl.unit.test.ts │ ├── buildGqlEndpoint.unit.test.ts │ ├── buildGqlHeaders.unit.test.ts │ ├── safelyGetNestedKey.unit.test.ts │ └── shouldFetchtranslation.unit.test.ts ├── tools └── migrateStoreData.js ├── tsconfig.json └── yarn.lock /.env.sample: -------------------------------------------------------------------------------- 1 | APP_URL= 2 | SHOPIFY_API_KEY= 3 | SHOPIFY_APP_SECRET= 4 | SHOPIFY_APP_SCOPES=read_products,write_products,read_customers,write_customers,read_orders,write_orders,read_themes,write_themes,read_content,write_content,read_locations,read_checkouts,write_checkouts 5 | 6 | # == App Key in Firestore == # 7 | 8 | APP_NAME_KEY= 9 | 10 | # == MongoDB == # 11 | MONGO_DB_CONNECTION_STRING= -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | node_modules/* -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "commonjs": true, 9 | "es6": true, 10 | "node": true, 11 | "jest/globals": true 12 | }, 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": 2018, 18 | "sourceType": "module" 19 | }, 20 | "parser": "babel-eslint", 21 | "plugins": [ 22 | "react", 23 | "jest" 24 | ], 25 | "settings": { 26 | "react": { 27 | "version": "detect" 28 | } 29 | }, 30 | "rules": { 31 | "no-extra-semi": "error", 32 | "no-dupe-keys": "error", 33 | "no-dupe-args": "error", 34 | "no-debugger" : "error", 35 | "no-duplicate-case" : "error", 36 | "no-console" : [1, { "allow": ["warn", "error", "info"] }], 37 | "no-undefined": "error", 38 | "quotes" : ["error", "single"], 39 | "camelcase": 1, 40 | "max-params": ["error", 5], 41 | "no-unused-vars": "warn", 42 | "indent": ["error", 2, {"SwitchCase": 1, "ignoredNodes": ["TemplateLiteral"]}], 43 | "no-empty": "error", 44 | "no-use-before-define": "error", 45 | "prefer-const": "error", 46 | "no-unreachable": "error", 47 | "valid-typeof": "error", 48 | "eqeqeq": "error", 49 | "no-var": "error", 50 | "keyword-spacing": ["warn", { "overrides": { 51 | "if": { "before": true, "after": false }, 52 | "for": { "before": false, "after": true }, 53 | "while": { "before": false, "after": true }, 54 | "from": {"before": true, "after": true }, 55 | "as": {"before": true, "after": true } 56 | }}] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add 'label1' to any changes within 'example' folder or any subfolders 2 | api: 3 | - pages/api/**/* 4 | 5 | webhooks: 6 | - pages/api/webhooke/**/* 7 | 8 | databse: 9 | - _utils/atlasMethods.ts 10 | 11 | shopifyMethods: 12 | - _utils/shopifyMethods.ts 13 | 14 | redux: 15 | - _store/**/* 16 | 17 | appConfig: 18 | - _config/**/* 19 | 20 | components: 21 | - components/**/* 22 | 23 | hooks: 24 | - hooks/**/* 25 | 26 | public: 27 | - public/**/* 28 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | # This workflow will triage pull requests and apply a label based on the 2 | # paths that are modified in the pull request. 3 | # 4 | # To use this workflow, you will need to set up a .github/labeler.yml 5 | # file with configuration. For more information, see: 6 | # https://github.com/actions/labeler/blob/master/README.md 7 | 8 | name: Labeler 9 | on: [pull_request] 10 | 11 | jobs: 12 | label: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/labeler@v2 18 | with: 19 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 20 | -------------------------------------------------------------------------------- /.github/workflows/units.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Units 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.16.1] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | # - run: npm run lint 29 | - run: npm run test:units:github-actions 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # built 28 | /dist 29 | 30 | # Firebase 31 | .runtimeconfig.json 32 | service-account.json 33 | 34 | ## 35 | /_config/firebaseConfig.ts 36 | .now -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.16.1 -------------------------------------------------------------------------------- /_config/billingOptions.ts: -------------------------------------------------------------------------------- 1 | export interface IFFeatureDetails { 2 | label: string, 3 | details?:string, 4 | } 5 | 6 | export interface IFBillingObject { 7 | id?: string | boolean, 8 | tier: string, 9 | label: string, 10 | description: string, 11 | descriptionTrial?: string, 12 | active: boolean | string, 13 | cost: number, 14 | includesLowerTiers?: string, 15 | features?: IFFeatureDetails[], 16 | trialLength: number, 17 | } 18 | 19 | const billingOptions: IFBillingObject[] = [ 20 | { 21 | id: false, 22 | tier: 'basic', 23 | label: 'Basic', 24 | description: 'Get started with out basic feature set', 25 | descriptionTrial: 'includes a 14 day free trial', 26 | active: false, 27 | trialLength: 14, 28 | cost: 10.00, 29 | features: [ 30 | { 31 | label: 'Awesome', 32 | details: 'Does awesome things!' 33 | } 34 | ] 35 | }, 36 | { 37 | id: false, 38 | tier: 'pro', 39 | label: 'Pro', 40 | description: 'Lets crank it up a level', 41 | active: false, 42 | trialLength: 0, 43 | cost: 30.00, 44 | includesLowerTiers: 'Includes everything from basic', 45 | features: [ 46 | { 47 | label: 'More awesome', 48 | details: 'Even more awesome stuff!' 49 | }, 50 | { 51 | label: 'Even More awesome', 52 | } 53 | ] 54 | }, 55 | { 56 | id: false, 57 | tier: 'Plus', 58 | label: 'plus', 59 | description: 'Designed for high volume stores', 60 | active: false, 61 | trialLength: 0, 62 | cost: 99.00, 63 | includesLowerTiers: 'Includes everything from pro', 64 | features: [ 65 | { 66 | label: 'Teir 2 Support', 67 | details: 'Tier two support and consultancy (billed) available' 68 | }, 69 | { 70 | label: 'Integration suppport', 71 | details: 'Additional consultancy for high level integrations' 72 | }, 73 | { 74 | label: 'Slack access', 75 | details: 'Speak directly to the team!' 76 | }, 77 | ] 78 | } 79 | ] 80 | 81 | export default billingOptions -------------------------------------------------------------------------------- /_config/config.ts: -------------------------------------------------------------------------------- 1 | 2 | type dbValues = 'atlas' 3 | 4 | interface IFappConfig { 5 | db: dbValues, 6 | dbName: string, 7 | dbRoot: string, 8 | 9 | dualAuth: boolean, 10 | 11 | forceDevelopment: boolean, 12 | } 13 | 14 | 15 | 16 | const appConfig: IFappConfig = { 17 | // DB 18 | db: 'atlas', // optional flag if you are gonna add a database type 19 | dbName: process.env.APP_NAME_KEY, 20 | dbRoot: 'stores', 21 | 22 | //Users 23 | dualAuth: true, // enables the dual bump auto for offline & online modes 24 | 25 | // Billing 26 | forceDevelopment: false, 27 | } 28 | 29 | export default appConfig 30 | 31 | -------------------------------------------------------------------------------- /_config/installInitialDataMongo.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | 3 | const installInitialDataMongo = (shop, token) => { 4 | 5 | return { 6 | _id: shop, 7 | shopifyApiToken: token, 8 | shopifyApiTokenError: false, 9 | callAuthenticityKey: uuidv4(), 10 | plan: { 11 | displayName: null, 12 | shopifyPlus: null, 13 | partnerDevelopment: null 14 | }, 15 | billing: { 16 | active: false, 17 | status: 'init' 18 | }, 19 | billingUsage: [], 20 | usersActive: false, 21 | users: [], 22 | } 23 | } 24 | 25 | export default installInitialDataMongo -------------------------------------------------------------------------------- /_constants/colorSets.ts: -------------------------------------------------------------------------------- 1 | // all the polaris pallets, these should not be mixed 2 | const colorSet = { 3 | sky : { 4 | 'Lighter': '#F9FAFB', 5 | 'Light': '#F4F6F8', 6 | 'Sky': '#DFE3E8', 7 | 'Dark': '#C4CDD5' 8 | }, 9 | ink: { 10 | 'Lightest': '#919EAB', 11 | 'Lighter': '#637381', 12 | 'Light': '#454F5B', 13 | 'Ink': '#212B36' 14 | }, 15 | titleBar:{ 16 | 'Light': '#B3B5CB', 17 | 'Title bar': '#43467F', 18 | 'Dark': '#1C2260', 19 | 'Darker': '#00044C' 20 | }, 21 | purple: { 22 | 'Lighter': '#F6F0FD', 23 | 'Light': '#E3D0FF', 24 | 'Purple': '#9C6ADE', 25 | 'Dark': '#50248F', 26 | 'Darker': '#230051', 27 | 'Text': '#50495A' 28 | }, 29 | indigo: { 30 | 'Lighter': '#F4F5FA', 31 | 'Light': '#B3BCF5', 32 | 'Indigo': '#5C6AC4', 33 | 'Dark': '#202E78', 34 | 'Darker': '#000639', 35 | 'Text': '#3E4155' 36 | }, 37 | blue: { 38 | 'Lighter': '#EBF5FA', 39 | 'Light': '#B4E1FA', 40 | 'Blue': '#006FBB', 41 | 'Dark': '#084E8A', 42 | 'Darker': '#001429', 43 | 'Text': '#3E4E57' 44 | }, 45 | teal: { 46 | 'Lighter': '#E0F5F5', 47 | 'Light': '#B7ECEC', 48 | 'Teal': '#47C1BF', 49 | 'Dark': '#00848E', 50 | 'Darker': '#003135', 51 | 'Text': '#405352' 52 | }, 53 | green: { 54 | 'Lighter': '#E3F1DF', 55 | 'Light': '#BBE5B3', 56 | 'Green': '#50B83C', 57 | 'Dark': '#108043', 58 | 'Darker': '#173630', 59 | 'Text': '#414F3E' 60 | }, 61 | yellow: { 62 | 'Lighter': '#FCF1CD', 63 | 'Light': '#FFEA8A', 64 | 'Yellow': '#EEC200', 65 | 'Dark': '#8A6116', 66 | 'Darker': '#573B00', 67 | 'Text': '#595130' 68 | }, 69 | orange: { 70 | 'Lighter': '#FCEBDB', 71 | 'Light': '#FFC58B', 72 | 'Orange': '#F49342', 73 | 'Dark': '#C05717', 74 | 'Darker': '#4A1504', 75 | 'Text': '#594430' 76 | }, 77 | red: { 78 | 'Lighter': '#FBEAE5', 79 | 'Light': '#FEAD9A', 80 | 'Red': '#DE3618', 81 | 'Dark': '#BF0711', 82 | 'Darker': '#330101', 83 | 'Text': '#583C35' 84 | } 85 | } 86 | 87 | 88 | 89 | export default colorSet -------------------------------------------------------------------------------- /_constants/index.ts: -------------------------------------------------------------------------------- 1 | 2 | const CONSTANTS = { 3 | // singles 4 | LOADING: 'LOADING', 5 | DATABASE_LOGIN: 'FIREBASE_LOGIN', 6 | DATABASE_LOGIN_RESET: 'FIREBASE_LOGIN_RESET', 7 | UPDATE_SHOP: 'UPDATE_SHOP', 8 | UPDATE_SHOP_DOMAIN: 'UPDATE_SHOP_DOMAIN', 9 | UPDATE_CURRENT_PATH: 'UPDATE_CURRENT_PATH', 10 | INSTALL_SET_DATA: 'INSTALL_SET_DATA', 11 | INSTALL_SET_DATA_RESET: 'INSTALL_SET_DATA_RESET', 12 | UPDATE_BILLING: 'UPDATE_BILLING', 13 | } 14 | 15 | export default CONSTANTS -------------------------------------------------------------------------------- /_gql/createBillingSubscription.ts: -------------------------------------------------------------------------------- 1 | const CREATE_APP_BILLIN_SUBSCRIPTION: string = `mutation appSubscriptionCreate( 2 | $name: String!, 3 | $lineItems: [AppSubscriptionLineItemInput!]!, 4 | $returnUrl: URL! 5 | $test: Boolean! 6 | $trialDays: Int! 7 | ) { 8 | appSubscriptionCreate( 9 | name: $name, 10 | lineItems: $lineItems, 11 | returnUrl: $returnUrl, 12 | test: $test, 13 | trialDays: $trialDays 14 | 15 | ) { 16 | appSubscription { 17 | name 18 | returnUrl 19 | id 20 | status 21 | createdAt 22 | trialDays 23 | } 24 | confirmationUrl 25 | userErrors { 26 | field 27 | message 28 | } 29 | } 30 | }` 31 | 32 | export default CREATE_APP_BILLIN_SUBSCRIPTION -------------------------------------------------------------------------------- /_gql/getActiveSubscription.ts: -------------------------------------------------------------------------------- 1 | const GET_ACTIVE_SUBSCRIPTION = `{ 2 | currentAppInstallation { 3 | activeSubscriptions { 4 | name 5 | returnUrl 6 | id 7 | status 8 | createdAt 9 | trialDays 10 | test 11 | } 12 | } 13 | }` 14 | 15 | export default GET_ACTIVE_SUBSCRIPTION -------------------------------------------------------------------------------- /_middleware/verifiedConnection.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { TverificationMiddleware, Thandler } from '../_types/verifiedConnections'; 3 | 4 | /** 5 | * verifyConnection 6 | * - verifies POST used as a method and 7 | * - verifies origin 8 | * - any bespoke actions 9 | */ 10 | 11 | const verifiedConnection: TverificationMiddleware = (handler: Thandler) => { 12 | 13 | return async(req: NextApiRequest, res: NextApiResponse) => { 14 | 15 | if(process.env.NODE_ENV === 'production') { 16 | 17 | const host = req.headers['x-forwarded-host'] 18 | const cleanBaseOrigin = process.env.APP_URL.replace('https://', '') 19 | 20 | // early respond for malicious & wrong methods of requests 21 | if(req.method !== 'POST' || host !== cleanBaseOrigin) { 22 | return res.status(429).json({error: true, message: 'Method not allowed or security check failed'}) 23 | } 24 | } 25 | 26 | return handler(req, res) 27 | } 28 | } 29 | 30 | 31 | export default verifiedConnection 32 | -------------------------------------------------------------------------------- /_middleware/verifiedWebhook.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next' 2 | import { createRawBody, checkWebhookHmacValidity } from 'shopify-hmac-validation' 3 | import { TverificationMiddleware, Thandler } from '../_types/verifiedConnections'; 4 | 5 | const verifiedWebhook: TverificationMiddleware = (handler: Thandler) => { 6 | 7 | return async (req: NextApiRequest, res: NextApiResponse) => { 8 | 9 | const hmac = req.headers['x-shopify-hmac-sha256'] 10 | const shop = req.headers['x-shopify-shop-domain'] 11 | const {shop_domain} = req.body 12 | 13 | if(!shop || !hmac || shop_domain !== shop) { 14 | return res.status(429).json({ 15 | body: 'Request could not be completed' 16 | }) 17 | } 18 | 19 | const rawBody = createRawBody(req.body) 20 | const isWebhookValid = checkWebhookHmacValidity(process.env.SHOPIFY_APP_SECRET, rawBody, hmac) 21 | 22 | if(!isWebhookValid) { 23 | console.error({error: true, req: {headers: req.headers, body: req.body, shop }}) 24 | // custom logging if you so choose 25 | return res.status(429).json({ 26 | body: 'Request is not validated' 27 | }) 28 | } 29 | 30 | return handler(req, res) 31 | } 32 | } 33 | 34 | export default verifiedWebhook -------------------------------------------------------------------------------- /_store/initialState.ts: -------------------------------------------------------------------------------- 1 | import appConfig from '../_config/config'; 2 | const initialState = { 3 | loading: false, 4 | app: { 5 | callAuthenticityKey: false, 6 | appUrl: process.env.APP_URL, 7 | k: process.env.SHOPIFY_API_KEY, 8 | environment: process.env.NODE_ENV === 'development' || appConfig.forceDevelopment, 9 | currentPath: { 10 | path: '/', 11 | href: '/' 12 | }, 13 | billing: 'init', 14 | }, 15 | shop: { 16 | domain: false, 17 | }, 18 | } 19 | 20 | export default initialState -------------------------------------------------------------------------------- /_store/reducers/app.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | import CONSTANTS from '../../_constants' 3 | 4 | export default produce((draft, action) => { 5 | switch (action.type) { 6 | 7 | // Router Sync 8 | case CONSTANTS.UPDATE_CURRENT_PATH: 9 | draft.currentPath = action.payload 10 | return draft 11 | 12 | // Installs 13 | case CONSTANTS.INSTALL_SET_DATA: 14 | draft = Object.assign({}, draft, action.payload) 15 | return draft 16 | 17 | case CONSTANTS.INSTALL_SET_DATA_RESET: 18 | draft = Object.assign({}, draft, { 19 | callAuthenticityKey: false, 20 | }) 21 | return draft 22 | 23 | case CONSTANTS.UPDATE_BILLING: 24 | draft = Object.assign({}, draft, { 25 | billing: action.payload, 26 | }) 27 | return draft 28 | 29 | 30 | } 31 | }) -------------------------------------------------------------------------------- /_store/reducers/loading.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | import CONSTANTS from '../../_constants' 3 | 4 | export default produce((draft, action) => { 5 | switch (action.type) { 6 | case CONSTANTS.LOADING: 7 | draft = action.payload 8 | return draft 9 | } 10 | }) -------------------------------------------------------------------------------- /_store/reducers/shop.tsx: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | import CONSTANTS from '../../_constants' 3 | 4 | export default produce((draft, action) => { 5 | switch (action.type) { 6 | case CONSTANTS.UPDATE_SHOP: 7 | draft = action.payload 8 | return draft 9 | case CONSTANTS.UPDATE_SHOP_DOMAIN: 10 | draft.domain = action.payload 11 | return draft 12 | } 13 | }) -------------------------------------------------------------------------------- /_store/rootReducer.ts: -------------------------------------------------------------------------------- 1 | import { immerCombineReducers } from 'immer-combine-reducers' 2 | import produce from 'immer' 3 | 4 | import loading from './reducers/loading' 5 | import shop from './reducers/shop' 6 | import app from './reducers/app' 7 | 8 | // This combines immer reducers 9 | const rootReducer = immerCombineReducers(produce, { 10 | app, 11 | loading, 12 | shop, 13 | }) 14 | 15 | 16 | export default rootReducer -------------------------------------------------------------------------------- /_store/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import { composeWithDevTools } from 'redux-devtools-extension' 3 | 4 | import initialStateFile from './initialState' 5 | import rootReducer from './rootReducer' 6 | 7 | 8 | export const initializeStore = (initialState = initialStateFile) => { 9 | return createStore( 10 | rootReducer, 11 | initialState, 12 | composeWithDevTools(applyMiddleware()) 13 | ) 14 | } -------------------------------------------------------------------------------- /_types/safelyGetNestedText.d.ts: -------------------------------------------------------------------------------- 1 | export type TgetNestedKey = (texkKey: string[], dictionary: Object) => string | false 2 | 3 | export type TsafelyGetNestedText = (texkKey: string, dictionary: Object) => string | false -------------------------------------------------------------------------------- /_types/shouldFetchTranslations.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export type TshouldFetchTrasnslation = (locale: string) => string | 'fallback' 3 | export type TTranslationKeys = string | 'cs'|'da'|'de'|'en'|'es'|'fi'|'fr'|'hi'|'it'|'ja'|'ko'|'ms'|'nb'|'nl'|'pl'|'pt-BR'|'pt-PT'|'sv'|'th'|'tr'|'zh-CN'|'zh-TW' 4 | export interface IFfetchTranslationsOptions { 5 | 'cs'?: string, 6 | 'da'?: string, 7 | 'de'?: string, 8 | 'en'?: string, 9 | 'es'?: string, 10 | 'fi'?: string, 11 | 'fr'?: string, 12 | 'hi'?: string, 13 | 'it'?: string, 14 | 'ja'?: string, 15 | 'ko'?: string, 16 | 'ms'?: string, 17 | 'nb'?: string, 18 | 'nl'?: string, 19 | 'pl'?: string, 20 | 'pt-BR'?: string, 21 | 'pt-PT'?: string, 22 | 'sv'?: string, 23 | 'th'?: string, 24 | 'tr'?: string, 25 | 'zh-CN'?: string, 26 | 'zh-TW'?: string, 27 | } -------------------------------------------------------------------------------- /_types/textProvider.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IFTextContext { 3 | children: any, 4 | locale?: string 5 | } 6 | 7 | export type Tt = (textKey: string) => string 8 | 9 | export type TtBlock = (textKey: string) => any 10 | 11 | export type TuseTranslation = () => any -------------------------------------------------------------------------------- /_types/verifiedConnections.d.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | export type Thandler = (req: NextApiRequest, res: NextApiResponse) => any 4 | 5 | export type TverificationMiddleware = (handler: Thandler) => any -------------------------------------------------------------------------------- /_utils/arrayContainsArray.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * arrayContainsArray 3 | * - Compares a subset of the master array for 4 | * - NOTE: Only works with simple arrays not arrays of objects they will always return false 5 | * 6 | * @param {array} superset - master array which will be compared against 7 | * @param {array} subset - list which is compared agains the superset 8 | */ 9 | 10 | const arrayContainsArray = (superset, subset) => subset.length === 0 ? false : subset.every((value) => (superset.indexOf(value) >= 0)) 11 | 12 | export default arrayContainsArray -------------------------------------------------------------------------------- /_utils/atlasMethods.ts: -------------------------------------------------------------------------------- 1 | import appConfig from '../_config/config'; 2 | import { MongoClient } from 'mongodb'; 3 | 4 | export const createDBClient = () => { 5 | const client: MongoClient = new MongoClient(process.env.MONGO_DB_CONNECTION_STRING, { useUnifiedTopology: true }) 6 | return client 7 | } 8 | 9 | export const listDatabases = async (client: MongoClient) => { 10 | 11 | const databasesList = await client.db().admin().listDatabases(); 12 | 13 | console.log('Databases:'); 14 | databasesList.databases.forEach(db => console.log(` - ${db.name}`)); 15 | } 16 | 17 | 18 | export const createStoreDocument = async (client: MongoClient, document: any) => { 19 | const result = await client.db(appConfig.dbName).collection(appConfig.dbRoot).insertOne(document); 20 | console.log(`New listing created with the following id: ${result.insertedId}`); 21 | return document.callAuthenticityKey 22 | } 23 | 24 | export const findOneStoreDocumentById = async (client: MongoClient, id: string) => { 25 | 26 | const result = await client.db(appConfig.dbName).collection(appConfig.dbRoot).findOne({ _id: id }); 27 | if(result) { 28 | console.log(`Found a listing in the collection with the name '${id}':`); 29 | return result 30 | } else { 31 | console.error(`No listings found with the name '${id}'`) 32 | return false 33 | } 34 | } 35 | 36 | export const getStoreTokenById = async (client: MongoClient, id: string) => { 37 | const result = await client.db(appConfig.dbName).collection(appConfig.dbRoot).findOne({ _id: id }) 38 | if(result) { 39 | const {shopifyApiToken, callAuthenticityKey} = result 40 | return {shopifyApiToken, callAuthenticityKey} 41 | } 42 | return false 43 | } 44 | 45 | export const updateField = async (client: MongoClient, id: string, field: string, payload: any) => { 46 | const result = await client.db(appConfig.dbName).collection(appConfig.dbRoot).updateOne({ _id: id }, {$set: { [field] : payload}}) 47 | console.log(`updating ${id}.${field}`) 48 | if(result) { 49 | return true 50 | } 51 | return false 52 | } 53 | 54 | 55 | export const deleteDocumentById = async (client: MongoClient, id: string) => { 56 | const result = await client.db(appConfig.dbName).collection(appConfig.dbRoot).deleteOne({_id: id}) 57 | if(result) { 58 | return result 59 | } 60 | return false 61 | } 62 | 63 | 64 | // Index 65 | const atlasMethods = { 66 | createDBClient, 67 | createStoreDocument, 68 | deleteDocumentById, 69 | findOneStoreDocumentById, 70 | getStoreTokenById, 71 | listDatabases, 72 | updateField, 73 | } 74 | 75 | 76 | 77 | export default atlasMethods -------------------------------------------------------------------------------- /_utils/buildAuthUrl.ts: -------------------------------------------------------------------------------- 1 | 2 | const buildAuthUrl = shop => shop ? `https://${shop}/admin/oauth/access_token` : '' 3 | 4 | export default buildAuthUrl -------------------------------------------------------------------------------- /_utils/buildGqlEndpoint.ts: -------------------------------------------------------------------------------- 1 | 2 | const buildGqlEndpoint = (shop: string, version: string = '2020-01') => shop ? `https://${shop}/admin/api/${version}/graphql.json` : '' 3 | 4 | export default buildGqlEndpoint -------------------------------------------------------------------------------- /_utils/buildGqlHeaders.ts: -------------------------------------------------------------------------------- 1 | const buildHeaders = (token: string) => token ? ({ 2 | 'X-Shopify-Access-Token': token, 3 | 'Content-Type': 'application/json', 4 | 'Accept': 'application/lson' 5 | }) : false 6 | 7 | export default buildHeaders -------------------------------------------------------------------------------- /_utils/buildHeaders.ts: -------------------------------------------------------------------------------- 1 | const buildHeaders = (token?: string) => token ? ({ 2 | 'X-Shopify-Access-Token': token, 3 | 'Content-Type': 'application/json', 4 | 'Accept': 'application/lson' 5 | }) : false 6 | 7 | export default buildHeaders -------------------------------------------------------------------------------- /_utils/buildInstallUrl.ts: -------------------------------------------------------------------------------- 1 | 2 | type TbuildIntallUrl = (shop: string, state: string, online?: boolean ) => string 3 | 4 | const buildInstallUrl:TbuildIntallUrl = (shop, state, online) => { 5 | return `https://${shop}/admin/oauth/authorize?client_id=${process.env.SHOPIFY_API_KEY}&scope=${process.env.SHOPIFY_APP_SCOPES}&state=${state}&redirect_uri=${process.env.APP_URL}/dashboard` 6 | } 7 | 8 | export default buildInstallUrl -------------------------------------------------------------------------------- /_utils/dataShapers/dataShapeBilling.ts: -------------------------------------------------------------------------------- 1 | import billingOptions from '../../_config/billingOptions'; 2 | 3 | const dataShapeBilling = (response) => { 4 | 5 | if(!response || !response.data || !response.data || !response.data.data.appSubscriptionCreate) { false } 6 | 7 | const subscriptionData = response.data.data.appSubscriptionCreate 8 | const {appSubscription : {status, id, trialDays, createdAt, name, returnUrl}, confirmationUrl} = subscriptionData 9 | 10 | const optionDetails = billingOptions.find(item => item.label === name) 11 | const {tier, cost} = optionDetails 12 | 13 | const billingObject = { 14 | active: false, // add active status 15 | cost, 16 | tier, 17 | createdAt, 18 | id, 19 | status, 20 | trialDays, 21 | label: name, 22 | confirmationUrl, 23 | } 24 | 25 | return billingObject 26 | } 27 | 28 | export default dataShapeBilling -------------------------------------------------------------------------------- /_utils/dataShapers/dataShapeBillingVerify.ts: -------------------------------------------------------------------------------- 1 | import billingOptions from '../../_config/billingOptions'; 2 | 3 | const dataShapeBillingVerify = (firstSubscription) => { 4 | 5 | const {status, id, trialDays, createdAt, name, returnUrl, test} = firstSubscription 6 | 7 | const optionDetails = billingOptions.find(item => item.label === name) 8 | const {tier, cost} = optionDetails 9 | 10 | const billingObject = { 11 | active: status, // add active status 12 | cost, 13 | tier, 14 | createdAt, 15 | id, 16 | status, 17 | trialDays, 18 | label: name, 19 | test, 20 | } 21 | 22 | return billingObject 23 | } 24 | 25 | export default dataShapeBillingVerify -------------------------------------------------------------------------------- /_utils/dataShapers/dataShapeCustomers.ts: -------------------------------------------------------------------------------- 1 | import { dataShaper } from './dataShapeProducts' 2 | 3 | const dataShapeCustomers: dataShaper = (response) => { 4 | 5 | if(!response || !response.data || !response.data.data || !response.data.data.customers) { return []} 6 | if(response.data.data.customers.edges.length === 0) { return []} 7 | 8 | try { 9 | const dataShaped = response.data.data.customers.edges.map(({node, cursor}) => { 10 | return Object.assign({}, {...node}, cursor ? {cursor} : null) 11 | }) 12 | return dataShaped 13 | } catch { 14 | return [] 15 | } 16 | } 17 | 18 | export default dataShapeCustomers -------------------------------------------------------------------------------- /_utils/dataShapers/dataShapeOders.ts: -------------------------------------------------------------------------------- 1 | import { dataShaper } from './dataShapeProducts' 2 | 3 | const dataShapeOrders: dataShaper = (response) => { 4 | 5 | if(!response || !response.data || !response.data.data || !response.data.data.orders) { return []} 6 | if(response.data.data.orders.edges.length === 0) { return []} 7 | 8 | try { 9 | const dataShaped = response.data.data.orders.edges.map(({node, cursor}) => { 10 | return Object.assign({}, {...node}, cursor ? {cursor} : null) 11 | }) 12 | return dataShaped 13 | } catch { 14 | return [] 15 | } 16 | } 17 | 18 | export default dataShapeOrders -------------------------------------------------------------------------------- /_utils/dataShapers/dataShapeProducts.ts: -------------------------------------------------------------------------------- 1 | 2 | export type dataShaper = (response: any) => any[] 3 | 4 | const dataShapeProducts: dataShaper = (response: any) => { 5 | 6 | if(!response || !response.data || !response.data.data || !response.data.data.products) { return []} 7 | if(response.data.data.products.edges.length === 0) { return []} 8 | 9 | try { 10 | const dataShaped = response.data.data.products.edges.map(({node, cursor}) => { 11 | return Object.assign({}, {...node}, cursor ? {cursor} : null) 12 | }) 13 | return dataShaped 14 | } catch { 15 | return [] 16 | } 17 | } 18 | 19 | export default dataShapeProducts -------------------------------------------------------------------------------- /_utils/safelyGetNestedText.ts: -------------------------------------------------------------------------------- 1 | import { TsafelyGetNestedText, TgetNestedKey } from '../_types/safelyGetNestedText'; 2 | 3 | export const getNestedKey: TgetNestedKey = (pathKeys, dictionary) => pathKeys.reduce((xs, x) => (xs && xs[x]) ? xs[x] : false, dictionary) 4 | 5 | // Safe function 6 | const safelyGetNestedText: TsafelyGetNestedText = (textKey, dictionary) => { 7 | 8 | if(typeof textKey !== 'string') { 9 | console.error('safelyGetNestedText requires a string key using object notation to the location you would like to retrieve') 10 | return `Could lookup translation at ${textKey}` 11 | } 12 | 13 | // just makes sure its an array 14 | const safePathKeys = textKey.split('.') 15 | 16 | return getNestedKey(safePathKeys, dictionary) 17 | 18 | } 19 | 20 | export default safelyGetNestedText -------------------------------------------------------------------------------- /_utils/shopifyMethods.ts: -------------------------------------------------------------------------------- 1 | import Axios from 'axios' 2 | import buildAuthUrl from './buildAuthUrl' 3 | import buildHeaders from './buildGqlHeaders' 4 | import buildGqlEndpoint from './buildGqlEndpoint' 5 | import GET_ACTIVE_SUBSCRIPTION from '../_gql/getActiveSubscription' 6 | 7 | // getting a token 8 | export const exchangeToken = async (shop, payload) => { 9 | try { 10 | const requestData = await Axios.post(buildAuthUrl(shop), payload) 11 | 12 | if(!requestData.data.access_token) {return false} 13 | 14 | return requestData.data.access_token 15 | } catch { 16 | return false 17 | } 18 | } 19 | 20 | // Get user token 21 | export const exchangeUserToken = async (shop, payload) => { 22 | try { 23 | const requestData = await Axios.post(buildAuthUrl(shop), payload) 24 | 25 | if(!requestData.data.access_token) {return false} 26 | 27 | return requestData.data 28 | } catch { 29 | return false 30 | } 31 | } 32 | 33 | // retrieving billing 34 | export const getCurrentAppBilling = async (shop, token) => { 35 | const headers = buildHeaders(token) 36 | 37 | const requestData = await Axios({ 38 | url: buildGqlEndpoint(shop), 39 | method: 'post', 40 | data: { 41 | query: GET_ACTIVE_SUBSCRIPTION, 42 | }, 43 | headers: headers 44 | }) 45 | 46 | // return stuff 47 | if(requestData.data) { 48 | return {...requestData.data.data.currentAppInstallation.activeSubscriptions} 49 | } 50 | 51 | return false 52 | 53 | } 54 | 55 | 56 | const shopifyMethods = { 57 | exchangeToken, 58 | exchangeUserToken, 59 | getCurrentAppBilling, 60 | } 61 | 62 | export default shopifyMethods -------------------------------------------------------------------------------- /_utils/shouldFetchtranslation.ts: -------------------------------------------------------------------------------- 1 | // change this to set the fallback translation 2 | import en from '../public/locales/en.json' 3 | import { IFfetchTranslationsOptions, TshouldFetchTrasnslation } from '../_types/shouldFetchTranslations'; 4 | 5 | // This is actually bundled with the app so no need to fetch 6 | // failing to set this will cause errors and failing tests 7 | export const fallbackLocale: string = 'en' 8 | export const fallbackPreloadedLibrary: any = en 9 | fallbackPreloadedLibrary 10 | // you can enable translations by adding their key and paths to this object. 11 | // fetched translations will be marginally slower 12 | // however at the benifit of a faster loading app 13 | // Remember to update the interface and the translation list 14 | 15 | const fetchTranslations: IFfetchTranslationsOptions = { 16 | fr: '/locales/fr.json', 17 | es: '/locales/es.json', 18 | de: '/locales/de.json' 19 | } 20 | 21 | 22 | 23 | const shouldFetchtranslation: TshouldFetchTrasnslation = (locale: string) => { 24 | 25 | if(locale.includes(fallbackLocale)) { 26 | return 'fallback' 27 | } 28 | 29 | // run against the active translations 30 | const keys = Object.keys(fetchTranslations) 31 | const found = keys.find(key => locale.includes(key)) 32 | 33 | if(found) { 34 | return fetchTranslations[found] 35 | } 36 | 37 | return 'fallback' 38 | } 39 | 40 | export default shouldFetchtranslation -------------------------------------------------------------------------------- /_utils/validateWebhook.ts: -------------------------------------------------------------------------------- 1 | import { createRawBody, checkWebhookHmacValidity } from 'shopify-hmac-validation' 2 | import { NextApiRequest } from 'next' 3 | 4 | type TvalidateWebhook = (req: NextApiRequest, hmac: string | string[]) => boolean 5 | 6 | const validateWebhook: TvalidateWebhook = (req, hmac) => { 7 | 8 | const rawBody = createRawBody(req.body) 9 | const isWebhookValid = checkWebhookHmacValidity(process.env.SHOPIFY_APP_SECRET, rawBody, hmac) 10 | 11 | return isWebhookValid 12 | 13 | } 14 | 15 | export default validateWebhook -------------------------------------------------------------------------------- /components/AppBridgeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react' 2 | import { useSelector } from 'react-redux'; 3 | import {Provider} from '@shopify/app-bridge-react' 4 | 5 | const AppBridgeProvider = ({children}) => { 6 | 7 | const permanentDomain = useSelector(state => state.shop.domain) 8 | const key = useSelector(state => state.app.k) 9 | 10 | 11 | if(!permanentDomain || !key) { 12 | return ( 13 | 14 | {children} 15 | 16 | ) 17 | } 18 | 19 | const appBridgeConfig = {apiKey: key, shopOrigin: permanentDomain, forceRedirect: true} 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ) 26 | 27 | 28 | } 29 | 30 | export default AppBridgeProvider -------------------------------------------------------------------------------- /components/CustomLink.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactChildren } from 'react' 2 | import Link from 'next/link' 3 | 4 | /** 5 | * Polaris links are now Next JS Links! 6 | */ 7 | 8 | interface CustomLinkProps { 9 | children?: React.ReactChildren, 10 | url: string, 11 | rest?: any 12 | } 13 | 14 | 15 | const CustomLink:React.FC = ({children, url, ...rest}) => { 16 | 17 | return ( 18 | 19 | 22 | {children} 23 | 24 | 25 | ) 26 | 27 | } 28 | 29 | export default CustomLink -------------------------------------------------------------------------------- /components/I18nProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import {I18nContext, I18nManager} from '@shopify/react-i18n' 3 | import useAppBridge from '../hooks/useAppBridge' 4 | import { TextProvider } from './TextProvider' 5 | 6 | 7 | const I81nProvider = ({children}) => { 8 | 9 | const {locale} = useAppBridge() 10 | const i18nManager = new I18nManager({locale, onError: (err) => console.error({err})}) 11 | 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ) 19 | } 20 | 21 | export default I81nProvider -------------------------------------------------------------------------------- /components/Navigation/TopNav.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { Tabs } from '@shopify/polaris'; 3 | import { useSelector } from 'react-redux'; 4 | import { useRouter } from 'next/router'; 5 | 6 | type TNavType = string 7 | 8 | interface IFRQuery { 9 | type?: string 10 | } 11 | 12 | interface IFcurrentPath { 13 | path?: string, 14 | href?: string, 15 | } 16 | 17 | interface EnumTabItem { 18 | id: string, 19 | content: string, 20 | accessibilityLabel: string, 21 | url: string, 22 | } 23 | 24 | interface EnumTabItems extends Array { } 25 | 26 | const tabs: EnumTabItems = [ 27 | { 28 | id: 'dashboard', 29 | content: 'Dashboard', 30 | accessibilityLabel: 'dashboard', 31 | url: '/dashboard' 32 | }, 33 | { 34 | id: 'settings', 35 | content: 'Settings', 36 | accessibilityLabel: 'settings', 37 | url: '/settings' 38 | }, 39 | { 40 | id: 'billing', 41 | content: 'Billing', 42 | accessibilityLabel: 'billing', 43 | url: '/billing' 44 | }, 45 | ] 46 | 47 | 48 | // Component 49 | const TopNav = () => { 50 | 51 | const [selected, setSelected] = useState(0) 52 | 53 | const router = useRouter() 54 | const currentPath: IFcurrentPath = useSelector(state => state.app.currentPath) 55 | 56 | const query: IFRQuery = router.query 57 | const navType: TNavType = query.type ? query.type : '' 58 | 59 | // Sync the index 60 | useEffect(() => { 61 | 62 | if (currentPath && currentPath.path) { 63 | const indexOfTab = tabs.findIndex(item => item.url.includes(currentPath.path)) 64 | if (indexOfTab !== -1 && selected !== indexOfTab) { 65 | setSelected(indexOfTab) 66 | } 67 | 68 | if (indexOfTab === -1 && navType) { 69 | const typedTab = tabs.findIndex(item => item.url.includes(navType)) 70 | if (typedTab !== -1) { 71 | setSelected(typedTab) 72 | } 73 | } 74 | } 75 | 76 | 77 | }, [currentPath, currentPath]) 78 | 79 | 80 | if (!tabs || tabs.length === 0) { 81 | return null 82 | } 83 | 84 | return ( 85 |
86 | 87 | 88 |
89 | ) 90 | } 91 | 92 | export default TopNav -------------------------------------------------------------------------------- /components/PolarisProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | 3 | import {AppProvider} from '@shopify/polaris' 4 | import CustomLink from './CustomLink' 5 | import { useTranslation } from './TextProvider'; 6 | 7 | 8 | const PolarisProvider = ({children}) => { 9 | 10 | const currentTranslation = useTranslation() // this will update according to the locale from the i8 provider 11 | 12 | return ( 13 | 17 | {children} 18 | 19 | ) 20 | } 21 | 22 | export default PolarisProvider -------------------------------------------------------------------------------- /components/Stage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react' 2 | import colorSet from '../_constants/colorSets' 3 | import {Page} from '@shopify/polaris' 4 | 5 | import Head from 'next/head' 6 | 7 | import useShopDomain from '../hooks/useShopDomain' 8 | import useRouterSync from '../hooks/useRouterSync' 9 | import useInstall from '../hooks/useShopData' 10 | 11 | import LoadingBar from './global/LoadingBar' 12 | import TopNav from './Navigation/TopNav' 13 | import { useSelector } from 'react-redux'; 14 | import LoadingPage from './global/LoadingPage' 15 | import BillingSelector from './billing/BillingSelector' 16 | 17 | 18 | 19 | const Stage = ({ children }) => { 20 | // Set Permanent domain if available 21 | const [domain] = useShopDomain() 22 | // Syncronizes the shopify navigation with the app current path 23 | const syncTheRoute = useRouterSync() 24 | // install or set call key 25 | const install = useInstall() 26 | 27 | const billing = useSelector(state => state.app.billing) 28 | 29 | const billingLoaded = billing !== 'init' 30 | const billingActive = billing.active 31 | 32 | return ( 33 | 34 |
35 | 36 | {billingLoaded && billingActive && } 37 | 38 | 47 |
48 | 49 | {/* Connections still loading */} 50 | {!billingLoaded && } 51 | 52 | {/* Main Return */} 53 | { billingLoaded &&
54 | 55 | 56 | 57 | App Boilerplate 58 | 59 | 60 | 61 | {/* Billing needs to be setup */} 62 | {billingLoaded && (!billing.active || billingActive === 'PENDING') && } 63 | 64 | {/* Billing all good */} 65 | {billingLoaded &&( billing.active && billingActive !== 'PENDING') && [children]} 66 | 67 | 68 | 69 | 74 | 75 |
} 76 | 77 |
78 | ) 79 | } 80 | 81 | export default Stage; 82 | -------------------------------------------------------------------------------- /components/TextProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, {createContext, useContext, useState, useEffect} from 'react' 2 | import fetch from 'isomorphic-unfetch' 3 | 4 | // Set defaults in shouldFetchtranslation 5 | import shouldFetchtranslation, {fallbackLocale, fallbackPreloadedLibrary} from '../_utils/shouldFetchtranslation' 6 | 7 | import safelyGetNestedText from '../_utils/safelyGetNestedText' 8 | import { IFTextContext, Tt, TtBlock } from '../_types/textProvider'; 9 | 10 | /** 11 | * Context 12 | */ 13 | export const TextContext: React.Context = createContext({}) 14 | 15 | /** 16 | * TextProvider - wrap the app nested below app bridge Component 17 | */ 18 | export const TextProvider: React.FC = ({children, locale}) => { 19 | 20 | const [currentLocale, setCurrentLocale] = useState(fallbackLocale) 21 | const [activeTranslation, setActiveTranslation] = useState(fallbackPreloadedLibrary) 22 | 23 | const updateCurrentLocale = async (locale: string) => { 24 | 25 | if(locale === currentLocale || locale.includes(currentLocale)) { 26 | console.log(`Using translations for ${locale}`) 27 | return false 28 | } 29 | 30 | const localePath = shouldFetchtranslation(locale) 31 | 32 | if(localePath !== 'fallback') { 33 | try { 34 | // reset 35 | setCurrentLocale(locale) 36 | const newTranslation = await fetch(localePath).then(r => r.json()) 37 | 38 | if(newTranslation) { 39 | console.log(`Swapping translation - ${locale}`) 40 | setActiveTranslation(newTranslation) 41 | return true 42 | } 43 | } catch (error) { 44 | console.error(error.message) 45 | } 46 | } 47 | 48 | // reset 49 | console.log(`Active translation not found for ${locale}, falling back to ${fallbackPreloadedLibrary}`) 50 | setCurrentLocale(fallbackPreloadedLibrary) 51 | setActiveTranslation(fallbackLocale) 52 | return true 53 | } 54 | 55 | useEffect(() => { 56 | 57 | if (locale !== currentLocale) { 58 | console.log('Running Update') 59 | updateCurrentLocale(locale) 60 | } 61 | 62 | }, [locale]) 63 | 64 | return ( 65 | 66 | {children} 67 | 68 | ) 69 | } 70 | 71 | /** 72 | * useTranslation - returns current translation dictionary 73 | */ 74 | export const useTranslation = () => useContext(TextContext).activeTranslation 75 | 76 | /** 77 | * T our text helper function that returns text strings singular. 78 | */ 79 | export const T: Tt = (textKey) => { 80 | 81 | const Context = useContext(TextContext) 82 | const dictionary = Context.activeTranslation 83 | const locale = Context.currentLocale 84 | const translatedValue = safelyGetNestedText(textKey, dictionary) 85 | 86 | if(translatedValue) { 87 | return translatedValue 88 | } 89 | 90 | console.log(`No translation found for ${locale} falling back to ${fallbackLocale}`) 91 | const fallbackTranslation = safelyGetNestedText(textKey, fallbackPreloadedLibrary) 92 | 93 | if(fallbackTranslation) { 94 | console.log(`Translation found in fallback dictionary ${fallbackLocale}`) 95 | return fallbackTranslation 96 | } 97 | 98 | // no TranslationFound 99 | return 'NO TRANSLATION FOUND' 100 | } 101 | /** 102 | * TBlock - returns a block of text ina single call for more performant text at a component level 103 | */ 104 | export const TBlock: TtBlock = (textKey) => { 105 | 106 | const Context = useContext(TextContext) 107 | const dictionary = Context.activeTranslation 108 | const locale = Context.currentLocale 109 | const translatedValue: any = safelyGetNestedText(textKey, dictionary) 110 | 111 | if(translatedValue) { 112 | return translatedValue 113 | } 114 | 115 | console.log(`No translation found for ${locale} falling back to ${fallbackLocale}`) 116 | const fallbackTranslation: any = safelyGetNestedText(textKey, fallbackPreloadedLibrary) 117 | 118 | if(fallbackTranslation) { 119 | console.log(`Translation found in fallback dictionary ${fallbackLocale}`) 120 | return fallbackTranslation 121 | } 122 | 123 | // no TranslationFound 124 | return 'NO BLOCK FOUND CHECK YOU LOCALES' 125 | } 126 | 127 | /** 128 | * The default index export of all methods and Components 129 | */ 130 | const TextProviderIndex = { 131 | TBlock, 132 | T, 133 | useTranslation, 134 | TextProvider, 135 | TextContext, 136 | } 137 | 138 | 139 | export default TextProviderIndex -------------------------------------------------------------------------------- /components/billing/BillingBannerInit.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Banner } from '@shopify/polaris' 3 | 4 | const BillingBannerInit = () => { 5 | return ( 6 | 10 | 11 | ) 12 | } 13 | 14 | export default BillingBannerInit -------------------------------------------------------------------------------- /components/billing/BillingCards.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Layout, Card, Heading, Badge, Stack, Button, TextStyle } from '@shopify/polaris' 3 | import { IFBillingObject } from '../../_config/billingOptions'; 4 | import { useSelector } from 'react-redux'; 5 | import { Features } from '@shopify/app-bridge/actions'; 6 | import BillingFeatureList from './BillingFeatureList'; 7 | import colorSet from '../../_constants/colorSets'; 8 | 9 | type TBillingitems = IFBillingObject[] | [] 10 | 11 | interface IFBillingCards { 12 | children?: any, 13 | items: TBillingitems, 14 | changePlan: any // import needed 15 | } 16 | 17 | 18 | const BillingCards:React.FC = ({items, changePlan}) => { 19 | 20 | const billing = useSelector(state => state.app.billing) 21 | const [expandedFeatures, setexpandedFeatures] = useState(false) 22 | 23 | if(!billing) { return null} 24 | 25 | const indexOfCurrent = items.findIndex((item: IFBillingObject) => (billing.active === 'ACTIVE' && item.tier === billing.tier) ) 26 | 27 | 28 | return ( 29 | 30 | { 31 | [...items].map((item: IFBillingObject, index) => { 32 | 33 | const isActive = billing.tier === item.tier && billing.active 34 | const isFree = item.tier === 'free' || item.cost === 0.00 35 | 36 | const shouldDowngrade = indexOfCurrent > index 37 | 38 | const buttonText = () => { 39 | 40 | if (isActive) { 41 | return 'Active Plan' 42 | } 43 | 44 | if (shouldDowngrade) { 45 | return 'Downgrade plan' 46 | } 47 | 48 | return 'Upgrade plan' 49 | } 50 | 51 | return ( 52 | 53 | 56 | 57 | 58 | 59 | 60 | {item.label} 61 | {isActive && !isFree && {billing.status}} 62 | 63 | 64 | 65 |
66 | {item.description} 67 | {!shouldDowngrade && item.descriptionTrial && !isActive && {item.descriptionTrial}} 68 |
69 | 70 | 71 | 72 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | {!isFree && {`$${item.cost}/month`}} 86 | 87 | 88 | {!isFree && 89 | 96 | } 97 | 98 | 99 |
100 | 101 | 122 | 123 |
124 | ) 125 | }) 126 | } 127 |
128 | ) 129 | 130 | } 131 | 132 | export default BillingCards -------------------------------------------------------------------------------- /components/billing/BillingFeatureList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IFFeatureDetails } from '../../_config/billingOptions'; 3 | import {TickMinor} from '@shopify/polaris-icons'; 4 | import { Icon, TextStyle } from '@shopify/polaris'; 5 | import colorSet from '../../_constants/colorSets'; 6 | 7 | export interface IFBillingFeatureList { 8 | features?: IFFeatureDetails[] 9 | expandedFeatures: boolean 10 | } 11 | 12 | const BillingFeatureList: React.SFC = ({features, expandedFeatures}) => { 13 | if(!features) { return null } 14 | 15 | return ( 16 |
    17 | 18 | {[...features].map((item: IFFeatureDetails, index: Number) => { 19 | return ( 20 |
  • 21 | 22 |
    23 | 24 |
    25 | 26 |
    27 | {item.label} 28 | {item.details && expandedFeatures && {item.details}} 29 |
    30 | 31 |
  • 32 | ) 33 | })} 34 | 35 | 56 |
57 | ) 58 | } 59 | 60 | export default BillingFeatureList -------------------------------------------------------------------------------- /components/billing/BillingSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { Layout, FooterHelp, Link } from '@shopify/polaris' 3 | import BillingCards from './BillingCards' 4 | import { useSelector, useDispatch } from 'react-redux'; 5 | import useBilling from '../../hooks/useBilling' 6 | import billingOptions from '../../_config/billingOptions'; 7 | import BillingBannerInit from './BillingBannerInit'; 8 | import CONSTANTS from '../../_constants'; 9 | 10 | const BillingSelector = () => { 11 | 12 | const dispatch = useDispatch() 13 | const loading = useSelector(state => state.laoding) 14 | 15 | const billing = useSelector(state => state.app.billing) 16 | const { changePlan, data, fetching, mustRedirect } = useBilling() 17 | if (billing === 'init') { return null } 18 | 19 | 20 | 21 | // set fetching 22 | useEffect(() => { 23 | if (loading !== fetching) { 24 | dispatch({ type: CONSTANTS.LOADING, payload: fetching }) 25 | } 26 | }, [fetching]) 27 | 28 | return ( 29 | 30 | 31 | {(billing.status === 'init' || !billing.active) && 32 | 33 | 34 | 35 | } 36 | 37 | 38 | 39 | 40 | Learn more about{' '} 41 | 42 | billing & charges 43 | 44 | 45 | 46 | ) 47 | } 48 | 49 | export default BillingSelector -------------------------------------------------------------------------------- /components/global/LoadingBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {useSelector} from 'react-redux' 3 | import colorSet from '../../_constants/colorSets' 4 | 5 | const LoadingBar = () => { 6 | 7 | // color 8 | const accent = colorSet.indigo.Indigo 9 | // state 10 | const loading = useSelector(state => state.loading) 11 | 12 | return ( 13 |
14 | {loading ? (Loading) : ''} 15 | 41 |
42 | ) 43 | } 44 | 45 | export default LoadingBar -------------------------------------------------------------------------------- /components/global/LoadingPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import colorSet from '../../_constants/colorSets' 3 | 4 | const LoadingPage = () => { 5 | 6 | return ( 7 |
8 | 9 | 10 | LOADING 11 | 12 | 13 | 51 |
52 | ) 53 | 54 | } 55 | 56 | export default LoadingPage -------------------------------------------------------------------------------- /hooks/useAppBridge.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from 'react' 2 | import {Context} from '@shopify/app-bridge-react' 3 | import { Redirect } from '@shopify/app-bridge/actions'; 4 | 5 | interface IFuseAppBridgeReturn { 6 | state?: any, 7 | appBridge: any, // type this looks to be missing 8 | } 9 | const useAppBridge = () => { 10 | 11 | const appBridge = useContext(Context) 12 | 13 | const [userLocale, setUserLocale] = useState('en') 14 | const [state, setState] = useState('en') 15 | 16 | const setStates = async () => { 17 | const state = await appBridge.getState() 18 | setState(state) 19 | setUserLocale(state.staffMember.locale) 20 | } 21 | 22 | const redirect = appBridge ? Redirect.create(appBridge) : {dispatch: () => {}} 23 | 24 | const redirectToRemote = (url) => { 25 | if(appBridge ) { 26 | redirect.dispatch(Redirect.Action.REMOTE, { 27 | url: url, 28 | newContext: true, 29 | }) 30 | } 31 | } 32 | 33 | useEffect(() => { 34 | 35 | if(appBridge) { 36 | setStates() 37 | } 38 | 39 | }, [appBridge]) 40 | 41 | return { 42 | appBridge, 43 | locale: userLocale, 44 | state, 45 | redirectToRemote, 46 | } 47 | } 48 | 49 | export default useAppBridge -------------------------------------------------------------------------------- /hooks/useBilling.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import billingOptions from '../_config/billingOptions' 3 | import Axios from 'axios' 4 | import CREATE_APP_BILLIN_SUBSCRIPTION from '../_gql/createBillingSubscription'; 5 | import { useSelector, useDispatch } from 'react-redux'; 6 | import dataShapeBilling from '../_utils/dataShapers/dataShapeBilling'; 7 | import CONSTANTS from '../_constants'; 8 | import useAppBridge from './useAppBridge'; 9 | import { Redirect } from '@shopify/app-bridge/actions'; 10 | 11 | type TchangePlan = (tier: string, disableTrial: boolean | undefined) => any 12 | 13 | interface IFReturnUseBilling { 14 | fetching: boolean 15 | changePlan: TchangePlan 16 | syncBillingInfo: () => any 17 | mustRedirect: string 18 | data: any 19 | error: boolean 20 | } 21 | 22 | 23 | const useBilling = () => { 24 | 25 | const dispatch = useDispatch() 26 | const {appBridge} = useAppBridge() 27 | 28 | // selectors 29 | const permanentDomain: string | false = useSelector(state => state.shop.domain) 30 | const appUrl: string = useSelector(state => state.app.appUrl) 31 | const key: string = useSelector(state => state.app.k) 32 | const billing: any = useSelector(state => state.app.billing) 33 | const cak: any = useSelector(state => state.app.callAuthenticityKey) 34 | 35 | // development test params will be used 36 | const isDev: string = useSelector(state => state.app.environment) 37 | 38 | // statesd 39 | const [fetching, setFetching] = useState(false) 40 | const [data, setData] = useState() 41 | const [error, setError] = useState() 42 | const [mustRedirect, setMustRedirect] = useState('init') 43 | 44 | // Change Plan 45 | const changePlan: TchangePlan = async (tier, disableTrial) => { 46 | 47 | if(!tier || !permanentDomain) { return false} 48 | 49 | const planDetails = billingOptions.find(item => item.tier === tier) 50 | if(!planDetails) { return false } 51 | 52 | 53 | const variables = { 54 | "trialDays": disableTrial ? 0 : planDetails.trialLength, 55 | "name": planDetails.label, 56 | "returnUrl": `${appUrl}/api/verifybilling?shop=${permanentDomain}&cak=${cak}`, 57 | "test": isDev, 58 | "lineItems": [{ 59 | "plan": { 60 | "appRecurringPricingDetails": { 61 | "price": { "amount": planDetails.cost, "currencyCode": "USD" } 62 | } 63 | } 64 | }] 65 | } 66 | 67 | // Run it 68 | try { 69 | setFetching(true) 70 | const response = await Axios.post('/api/query', { 71 | gql: CREATE_APP_BILLIN_SUBSCRIPTION, 72 | shop: permanentDomain, 73 | variables: variables, 74 | updateDb: 'billing' 75 | }) 76 | 77 | if(response && response.data) { 78 | const shapedData = dataShapeBilling(response) 79 | // @ts-ignore 80 | setData(shapedData) 81 | setFetching(false) 82 | if(shapedData.confirmationUrl) { 83 | setMustRedirect(shapedData.confirmationUrl) 84 | } 85 | } 86 | 87 | } catch (error) { 88 | console.error('could not create subscription', error.message) 89 | } 90 | } 91 | 92 | // syncBillingInfo 93 | const syncBillingInfo = async () => {} 94 | 95 | useEffect(() => { 96 | // @ts-ignore 97 | if(data && !data.active && mustRedirect) { 98 | if(mustRedirect){ 99 | if(!fetching && mustRedirect !== 'init') { 100 | if(typeof window !== 'undefined' && window.location) { 101 | appBridge.dispatch( 102 | Redirect.toRemote({ 103 | url: mustRedirect 104 | }) 105 | ) 106 | } 107 | } 108 | } else { 109 | dispatch({type: CONSTANTS.UPDATE_BILLING, payload: data}) 110 | } 111 | } 112 | 113 | }, [data, mustRedirect]) 114 | 115 | const returnObject: IFReturnUseBilling = { 116 | changePlan, 117 | data, 118 | error, 119 | fetching, 120 | mustRedirect, 121 | syncBillingInfo, 122 | } 123 | 124 | return returnObject 125 | 126 | } 127 | 128 | export default useBilling -------------------------------------------------------------------------------- /hooks/useMutation.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { useSelector, useDispatch } from 'react-redux' 3 | import Axios from 'axios' 4 | import CONSTANTS from '../_constants' 5 | import { IFPageOptions } from './useQuery'; 6 | 7 | interface IFUseMutationReturn { 8 | data?: any, 9 | error: boolean, 10 | fetching: boolean, 11 | runBulkMutation: (type: string, array: any[]) => any, 12 | runMutation: (mutation?: string, variables?: any) => any, 13 | } 14 | 15 | 16 | const useMutation = (query: string, variables: any, {shaper, pageOptions}: IFPageOptions) => { 17 | 18 | const dispatch = useDispatch() 19 | 20 | const permanentDomain = useSelector(state => state.shop.domain) 21 | 22 | const [fetching, setFetching] = useState(false) 23 | const [error, setError] = useState(false) 24 | const [data, setData] = useState() 25 | 26 | 27 | /** 28 | * runMutation 29 | * - Will run the mutation to be triggered on apply mostly used for single queries 30 | * - bulk will be run server side. 31 | * 32 | * @param {GQLquery} mutation - mutation string query defaults to the instantiated query 33 | * @param {object} variables - this is a set of specific variables 34 | */ 35 | const runMutation = async (mutation = query, vars = variables) => { 36 | 37 | if(fetching || !permanentDomain) { return false} 38 | 39 | try { 40 | setFetching(true) 41 | setError(false) 42 | 43 | // query 44 | const response = await Axios.post('/api/query', { 45 | gql: mutation, 46 | shop: permanentDomain, 47 | variables: vars 48 | }) 49 | 50 | if(response.data) { setData(shaper ? shaper(response) : response) } 51 | setFetching(false) 52 | return true 53 | 54 | } catch (error) { 55 | // when the $417 hits the ban 56 | setFetching(false) 57 | setError(true) 58 | return false 59 | } 60 | 61 | } 62 | 63 | // Placeholder for queued mutations 64 | const runBulkMutation = (type, array) => {} 65 | 66 | useEffect(() => { 67 | dispatch({type: CONSTANTS.LOADING, payload: fetching}) 68 | }, [fetching]) 69 | 70 | 71 | // Return 72 | const hookReturn: IFUseMutationReturn = { 73 | data, 74 | error, 75 | fetching, 76 | runBulkMutation, 77 | runMutation 78 | } 79 | 80 | return hookReturn 81 | } 82 | 83 | export default useMutation -------------------------------------------------------------------------------- /hooks/useQuery.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { useSelector } from 'react-redux' 3 | import Axios from 'axios' 4 | import merge from 'deepmerge' 5 | 6 | export interface IFPageOptions { 7 | shaper?: (resonse: any) => any, 8 | pageOptions?: { 9 | key?: string, 10 | } 11 | } 12 | 13 | interface IFUseQuery { 14 | query: string 15 | variables: any 16 | options: IFPageOptions 17 | } 18 | 19 | interface IFUseQueryReturn { 20 | fetching: boolean, 21 | error: boolean, 22 | data: any, 23 | runQuery: (variables?: any) => any, 24 | fetchMore: (variables?: any) => any, 25 | first: boolean, 26 | cursors: { 27 | next: string, 28 | prev: string, 29 | }, 30 | pageInfo?: { 31 | hasNextPage?: boolean, 32 | hasPreviousPage?: boolean, 33 | } 34 | } 35 | 36 | 37 | const useQuery = (query: string, variables: any, { shaper, pageOptions }: IFPageOptions) => { 38 | 39 | // used to check that all the state is set 40 | const permanentDomain = useSelector(state => state.shop.domain) 41 | 42 | // states 43 | const [fetching, setFetching] = useState(false) 44 | const [error, setError] = useState(false) 45 | const [data, setData] = useState() 46 | const [first, setFirst] = useState(true) 47 | const [cursors, setCursors] = useState({ next: '', prev: '' }) 48 | const [pageInfo, setPageInfo] = useState({ hasNextPage: false, hasPreviousPage: false }) 49 | 50 | /** 51 | * defines next & previous and sets the value 52 | * - Only triggered when a shaper is used 53 | * @param {array} shapedRespnse 54 | */ 55 | 56 | const defineAndSetCursors = (shapedRespnse) => { 57 | if (shapedRespnse[0].cursor) { 58 | const indexOfLast = shapedRespnse.length - 1 59 | const lastCursor = shapedRespnse[indexOfLast].cursor 60 | 61 | const indexOfPrevCursor = 0 62 | const prevCursor = shapedRespnse[indexOfPrevCursor].cursor 63 | 64 | setCursors({ next: lastCursor, prev: prevCursor }) 65 | } 66 | } 67 | 68 | 69 | /** 70 | * defineAndSetPageInfo 71 | * - defines and sets the page next and previos 72 | * @param {object} response - raw response to plug page info from 73 | */ 74 | const defineAndSetPageInfo = (response) => { 75 | if (!pageOptions) { return false } 76 | 77 | const { key } = pageOptions 78 | if (response.data.data[key] && response.data.data[key].pageInfo) { 79 | const { hasNextPage, hasPreviousPage } = response.data.data[key].pageInfo 80 | setPageInfo({ hasNextPage, hasPreviousPage }) 81 | } 82 | } 83 | 84 | /** 85 | * Run Query 86 | * - Runs a graphql query through the middleware 87 | * @param {Object} newVariables - override variables 88 | */ 89 | 90 | const runQuery = async (newVariables?: any) => { 91 | if (fetching || !permanentDomain) { return false } 92 | 93 | try { 94 | setFetching(true) 95 | setError(false) 96 | // request 97 | const queryVariables = newVariables ? newVariables : variables 98 | 99 | const response = await Axios.post('/api/query', { 100 | gql: query, 101 | shop: permanentDomain, 102 | variables: queryVariables ? queryVariables : {} 103 | }) 104 | 105 | // Set Data 106 | if (response.data) { setData(shaper ? shaper(response) : response) } 107 | 108 | // Set cursors 109 | if (shaper) { 110 | const cursorData = shaper(response) 111 | defineAndSetCursors(cursorData) 112 | } 113 | 114 | // set page information 115 | if (pageOptions) { 116 | defineAndSetPageInfo(response) 117 | } 118 | 119 | setFetching(false) 120 | } catch { 121 | // stuff when it fails. 122 | setFetching(false) 123 | setError(true) 124 | } 125 | } 126 | 127 | /** 128 | * fetchMore 129 | * Runs a graphql query through the middleware and aggregates the data into the response 130 | * @param {object} variables - object of variables to be passed to the query for fetching additional dat 131 | */ 132 | 133 | const fetchMore = async (newVariables) => { 134 | 135 | if (fetching || !newVariables || !permanentDomain) { return false } 136 | 137 | try { 138 | setFetching(true) 139 | setError(false) 140 | 141 | const response = await Axios.post('/api/query', { 142 | gql: query, 143 | shop: permanentDomain, 144 | variables: newVariables 145 | }) 146 | 147 | if (response.data) { 148 | const mergedData: any = shaper ? merge(data, shaper(response)) : merge(data, response) 149 | setData(mergedData) 150 | } 151 | 152 | // Set cursors 153 | if (shaper) { 154 | const cursorData = shaper(response) 155 | defineAndSetCursors(cursorData) 156 | } 157 | 158 | // set page information 159 | if (pageOptions) { 160 | defineAndSetPageInfo(response) 161 | } 162 | 163 | setFetching(false) 164 | 165 | } catch { 166 | setFetching(false) 167 | setError(true) 168 | } 169 | 170 | } 171 | 172 | // Run the query on first load. 173 | useEffect(() => { 174 | 175 | if (first && permanentDomain) { 176 | runQuery() 177 | setFirst(false) 178 | } 179 | 180 | return () => { } 181 | }, [permanentDomain]) 182 | 183 | 184 | const hookReturns: IFUseQueryReturn = { fetching, error, data, runQuery, fetchMore, first, cursors, pageInfo } 185 | return hookReturns 186 | } 187 | 188 | export default useQuery -------------------------------------------------------------------------------- /hooks/useRouterSync.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react' 2 | import { History } from '@shopify/app-bridge/actions' 3 | import createApp from '@shopify/app-bridge' 4 | import { HistoryAction } from '@shopify/app-bridge/actions/Navigation/History' 5 | import {useDispatch, useSelector} from 'react-redux' 6 | import CONSTANTS from '../_constants' 7 | import qs from 'query-string' 8 | 9 | /** 10 | * Keeps shopify and the iframe in sync. 11 | */ 12 | 13 | // @todo move to using useAppBridge rather than its own instantiation 14 | 15 | const useRouterSync = () => { 16 | 17 | const dispatch = useDispatch() 18 | const permanentDomain = useSelector(state => state.shop.domain) 19 | const shopifyKey = useSelector(state => state.app.k) 20 | 21 | useEffect(() => { 22 | if(window && permanentDomain) { 23 | 24 | const query = qs.parse(window.location.search) 25 | const path = `${window.location.pathname}${window.location.search}` 26 | const pathClean = `${window.location.pathname}` 27 | const shopifyHistoryActions = app => History.create(app) 28 | const shopifyAppBridge = permanentDomain ? createApp({apiKey: shopifyKey, shopOrigin: permanentDomain}) : false 29 | const shopifyHistory = shopifyAppBridge ? shopifyHistoryActions(shopifyAppBridge) : false 30 | 31 | if(shopifyHistory) { 32 | shopifyHistory.dispatch(History.Action.PUSH, path) 33 | dispatch({type: CONSTANTS.UPDATE_CURRENT_PATH, payload: {path: pathClean, href: window.location.href, query}}) 34 | console.log('SHOPIFY NAV UPDATED: ', path) 35 | } 36 | } 37 | }, [permanentDomain]) 38 | 39 | return [] 40 | } 41 | 42 | export default useRouterSync -------------------------------------------------------------------------------- /hooks/useShopData.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import qs from 'query-string' 4 | import Axios from 'axios'; 5 | import CONSTANTS from '../_constants'; 6 | 7 | 8 | const useInstall = () => { 9 | 10 | const dispatch = useDispatch() 11 | const permanentDomain = useSelector(state => state.shop.domain) 12 | const callAuthenticityKey = useSelector(state => state.app.callAuthenticityKey) 13 | const [callKey, setCallKey] = useState('') 14 | const [first, setFirst] = useState(true) 15 | 16 | 17 | const installOrData = async (code) => { 18 | 19 | if(callAuthenticityKey) { return true } 20 | 21 | try { 22 | dispatch({type: CONSTANTS.LOADING, payload: true}) 23 | const response = await Axios.post('api/install', {shop: permanentDomain, code}) 24 | if(response.data.body.callAuthenticityKey){ 25 | dispatch({type: CONSTANTS.INSTALL_SET_DATA, payload: {...response.data.body}}) 26 | dispatch({type: CONSTANTS.LOADING, payload: false}) 27 | setCallKey(response.data.body.callAuthenticityKey) 28 | return response.data.body.callAuthenticityKey 29 | } 30 | // no return 31 | } catch (error) { 32 | dispatch({type: CONSTANTS.LOADING, payload: false}) 33 | return false 34 | } 35 | } 36 | 37 | useEffect(() => { 38 | 39 | if(typeof window !== 'undefined' && window.location) { 40 | const query = qs.parse(window.location.search) 41 | const {code} = query 42 | if(first && permanentDomain) { 43 | installOrData(code) 44 | } 45 | } 46 | 47 | }, [first, permanentDomain, callKey]) 48 | 49 | return callKey 50 | 51 | } 52 | 53 | export default useInstall -------------------------------------------------------------------------------- /hooks/useShopDomain.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react' 2 | import qs from 'query-string' 3 | import {useDispatch, useSelector} from 'react-redux' 4 | import CONSTANTS from '../_constants' 5 | /** 6 | * IF the shop= query exists it will catch redux up automatically. 7 | */ 8 | const useShopDomain = () => { 9 | 10 | const dispatch = useDispatch() 11 | const permanentDomain = useSelector(state => state.shop.domain) 12 | 13 | useEffect(() => { 14 | // Only run when non SSR 15 | if(typeof window !== 'undefined' && window.location) { 16 | const query = qs.parse(window.location.search) 17 | // Sets the shop on auth redirect. 18 | // does not set on index. 19 | if(query.shop && !permanentDomain && window.location.pathname !== '/') { 20 | dispatch({type: CONSTANTS.UPDATE_SHOP_DOMAIN, payload: query.shop}) 21 | } 22 | } 23 | }, [permanentDomain]) 24 | 25 | return [permanentDomain] 26 | } 27 | 28 | export default useShopDomain -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/default', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { parsed: localEnv } = require('dotenv').config() 2 | const webpack = require('webpack') 3 | const path = require('path') 4 | module.exports = { 5 | webpack(config) { 6 | config.plugins.push(new webpack.EnvironmentPlugin(localEnv)) 7 | return config 8 | } 9 | } -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":2, 3 | "env": { 4 | "APP_URL": "@app_url_boilerplate", 5 | "SHOPIFY_API_KEY": "@shopify_api_key_boilerplate", 6 | "SHOPIFY_APP_SECRET": "@shopify_app_secret_boilerplate", 7 | "SHOPIFY_APP_SCOPES": "@shopify_app_scopes_boilerplate", 8 | 9 | "APP_NAME_KEY": "@app_name_key_boilerplate", 10 | "MONGO_DB_CONNECTION_STRING": "@mongo_db_connection_string_boilerplate" 11 | 12 | }, 13 | "routes": [ 14 | { 15 | "src": "/.*", 16 | "headers": { 17 | "Content-Security-Policy": "frame-ancestors https://*.shopify.com https://*.myshopify.com self", 18 | "Content-Security-Policy": "frame-src https://*.shopify.com https://*.myshopify.com self" 19 | }, 20 | "continue": true 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopify-app-boilerplate-v2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "ngrok": "ngrok http 3000", 10 | "lint": "eslint \"./**/*.ts\"", 11 | "lint:fix": "eslint 1--fix \"./**/*.ts\" ", 12 | "test:ci": "node tools/tests", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:units": "jest .*.unit.test.ts", 16 | "test:units:github-actions": "jest .*.unit.test.ts" 17 | }, 18 | "dependencies": { 19 | "@shopify/app-bridge": "^1.20.1", 20 | "@shopify/app-bridge-react": "^1.20.1", 21 | "@shopify/polaris": "^4.16.1", 22 | "@shopify/react-i18n": "^2.4.0", 23 | "@types/mongodb": "^3.5.4", 24 | "axios": "^0.19.2", 25 | "deepmerge": "^4.2.2", 26 | "dotenv": "^8.2.0", 27 | "immer": "^6.0.2", 28 | "immer-combine-reducers": "^1.0.1", 29 | "isomorphic-unfetch": "^3.0.0", 30 | "mongodb": "^3.5.5", 31 | "next": "9.3.4", 32 | "next-redux-wrapper": "^5.0.0", 33 | "nonce": "^1.0.4", 34 | "query-string": "^6.11.1", 35 | "react": "16.13.1", 36 | "react-dom": "16.13.1", 37 | "react-redux": "^7.2.0", 38 | "redux": "^4.0.5", 39 | "redux-devtools-extension": "^2.13.8", 40 | "shopify-hmac-validation": "^1.0.4", 41 | "uuid": "^7.0.2" 42 | }, 43 | "devDependencies": { 44 | "@babel/preset-env": "^7.9.0", 45 | "@babel/preset-typescript": "^7.9.0", 46 | "@types/jest": "^25.1.4", 47 | "@types/node": "^13.9.6", 48 | "@types/react": "^16.9.27", 49 | "babel-eslint": "^10.1.0", 50 | "cross-env": "^5.2.0", 51 | "eslint": "^6.8.0", 52 | "eslint-plugin-jest": "^23.8.2", 53 | "eslint-plugin-react": "^7.19.0", 54 | "husky": "^4.2.3", 55 | "jest": "^25.2.4", 56 | "ngrok": "^3.2.7", 57 | "ts-jest": "^25.3.0", 58 | "typescript": "^3.8.3" 59 | }, 60 | "husky": { 61 | "hooks": { 62 | "pre-push": "npm run lint" 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | // pages/_app.jsx 2 | import React from 'react' 3 | import {Provider} from 'react-redux' 4 | import App from 'next/app' 5 | import withRedux from 'next-redux-wrapper' 6 | import qs from 'query-string' 7 | import { initializeStore } from '../_store/store' 8 | 9 | import AppBridgeProvider from '../components/AppBridgeProvider' 10 | import '@shopify/polaris/styles.css' 11 | import PolarisProvider from '../components/PolarisProvider' 12 | import I81nProvider from '../components/I18nProvider' 13 | import { TextProvider } from '../components/TextProvider' 14 | 15 | // import InstallProvider from '../components/InstallProvider' 16 | 17 | class MyApp extends App { 18 | 19 | 20 | static async getInitialProps({Component, ctx}) { 21 | // We can dispatch from here too 22 | const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {}; 23 | 24 | return {pageProps}; 25 | } 26 | 27 | // Keep shopify in sync 28 | 29 | render() { 30 | 31 | // @ts-ignore 32 | const {Component, pageProps, store} = this.props 33 | 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | } 47 | 48 | export default withRedux(initializeStore)(MyApp) -------------------------------------------------------------------------------- /pages/api/auth.ts: -------------------------------------------------------------------------------- 1 | import {checkHmacValidity} from 'shopify-hmac-validation' 2 | import verifiedConnection from '../../_middleware/verifiedConnection'; 3 | 4 | const authHandler = async (req, res) => { 5 | 6 | if(!req.body.query.shop || !req.body.state) { 7 | console.error('Missing query data', req.body); 8 | return res.status(429).json({message:'Unauthorized: Required Query or Shop missing.'}) 9 | } 10 | 11 | const shopifyValidity = checkHmacValidity(process.env.SHOPIFY_APP_SECRET, req.body.query) 12 | 13 | if(!shopifyValidity) { 14 | console.error('Is HMAC is not valid', req.body.query.shop); 15 | return res.status(429).json({message:'Unauthorized: Invalid entrance detected'}) 16 | } 17 | 18 | const buildInstallUrl = (shop, state) => (`https://${shop}/admin/oauth/authorize?client_id=${process.env.SHOPIFY_API_KEY}&scope=${process.env.SHOPIFY_APP_SCOPES}&state=${state}&redirect_uri=${process.env.APP_URL}/dashboard`) 19 | 20 | const redirectTo = buildInstallUrl(req.body.query.shop, req.body.state) 21 | 22 | return res.status(200).json({ 23 | body: req.body, 24 | hmacValid: shopifyValidity, 25 | redirectTo: redirectTo 26 | }) 27 | } 28 | 29 | 30 | export default verifiedConnection(authHandler) -------------------------------------------------------------------------------- /pages/api/install.ts: -------------------------------------------------------------------------------- 1 | import {MongoClient} from 'mongodb' 2 | import shopifyMethods from '../../_utils/shopifyMethods' 3 | import { listDatabases, createStoreDocument, findOneStoreDocumentById, createDBClient } from '../../_utils/atlasMethods'; 4 | 5 | import installInitialDataMongo from '../../_config/installInitialDataMongo'; 6 | import { NextApiRequest, NextApiResponse } from 'next'; 7 | import verifiedConnection from '../../_middleware/verifiedConnection'; 8 | 9 | // Installs or returns the core shop data 10 | const InstallHandler = async (req: NextApiRequest, res: NextApiResponse) => { 11 | 12 | // required params 13 | const {shop, code} = req.body 14 | 15 | if(!req.body.shop ) { 16 | console.error('Missing query data', req.body); 17 | return res.status(429).json({message:'Unauthorized: Shop'}) 18 | } 19 | 20 | 21 | 22 | const client = createDBClient() 23 | 24 | // Connect 25 | try { 26 | // Connect to the MongoDB cluster 27 | await client.connect(); 28 | 29 | // Get the doc or create 30 | const storeDocument = await findOneStoreDocumentById(client, shop) 31 | 32 | if(storeDocument) { 33 | // store exists return early 34 | return res.status(200).json({ 35 | body: { 36 | callAuthenticityKey : storeDocument.callAuthenticityKey, 37 | billing: storeDocument.billing, 38 | billingCheckRequired: true, 39 | }, 40 | }) 41 | 42 | } 43 | 44 | // shopify token exchange 45 | if(!code) { 46 | return res.status(404).json({ 47 | error: true, 48 | message: 'code is missing' 49 | }) 50 | 51 | } 52 | 53 | // shopify token 54 | const token = await shopifyMethods.exchangeToken(shop, 55 | { 56 | client_id: process.env.SHOPIFY_API_KEY, 57 | client_secret: process.env.SHOPIFY_APP_SECRET, 58 | code 59 | }) 60 | 61 | if(!token) { 62 | return res.status(429).json({ 63 | error: true, 64 | message: 'Something has gone wrong exchanging shopify trroken' 65 | }) 66 | } 67 | 68 | // create db listing 69 | await createStoreDocument(client,installInitialDataMongo(shop, token)) 70 | // optionally create any other placeholders 71 | 72 | const newStore = await findOneStoreDocumentById(client, shop) 73 | 74 | if(newStore) { 75 | return res.status(200).json({ 76 | body: {callAuthenticityKey: newStore.callAuthenticityKey, billing: storeDocument.billing}, 77 | }) 78 | } 79 | 80 | 81 | } catch (e) { 82 | 83 | console.error(e); 84 | return res.status(500).json({ 85 | body: {error:true, message: e.message}, 86 | }) 87 | 88 | } finally { 89 | await client.close() 90 | } 91 | 92 | } 93 | 94 | export default verifiedConnection(InstallHandler) -------------------------------------------------------------------------------- /pages/api/query.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import {MongoClient} from 'mongodb' 3 | import buildHeaders from '../../_utils/buildHeaders' 4 | import buildGqlEndpoint from '../../_utils/buildGqlEndpoint' 5 | import atlasMethods from '../../_utils/atlasMethods' 6 | import dataShapeBilling from '../../_utils/dataShapers/dataShapeBilling' 7 | import { createDBClient } from '../../_utils/atlasMethods'; 8 | import verifiedConnection from '../../_middleware/verifiedConnection' 9 | 10 | 11 | const query = async (req, res) => { 12 | 13 | // no body sent 14 | if(!req.body) { 15 | return res.status(400).json({error: true, message: 'No request submitted for handling.'}) 16 | } 17 | 18 | // destructure request body 19 | const {shop, gql, variables, updateDb} = req.body 20 | 21 | // if (call Auth !== callAuth) {} 22 | 23 | // Validate Incoming 24 | if(!shop || !gql) { 25 | return res.status(400).json({error: true, message: 'Missing or incorrect parameters supplied'}) 26 | } 27 | 28 | const client = createDBClient() 29 | 30 | try { 31 | 32 | await client.connect() 33 | 34 | const storeTokenData = await atlasMethods.getStoreTokenById(client, shop) 35 | 36 | if(!storeTokenData) { 37 | return res.status(400).json({error: true, message: 'Missing token from the db'}) 38 | } 39 | 40 | const headers = storeTokenData ? buildHeaders(storeTokenData.shopifyApiToken) : false 41 | if(!headers) { return res.status(400).json({error: true, message: 'Headers could not be built'})} 42 | 43 | const shopifyResponse = await axios({ 44 | url: buildGqlEndpoint(shop), 45 | method: 'post', 46 | data: { 47 | query: gql, 48 | variables: variables ? variables : {} 49 | }, 50 | headers: headers 51 | }) 52 | 53 | // Early return 54 | if(!shopifyResponse.data) { 55 | return res.status(417).json({error: true, message: 'Data was not returned from shopify'}) 56 | } 57 | 58 | if(updateDb) { 59 | // @todo move to a switch or do something less rudimentary 60 | if(updateDb === 'billing') { 61 | await atlasMethods.updateField(client, shop, updateDb, dataShapeBilling(shopifyResponse)) 62 | } 63 | } 64 | 65 | return res.status(200).json({ ...shopifyResponse.data }) 66 | 67 | } catch (error) { 68 | 69 | console.error(error) 70 | return res.status(error.status ? error.status : 500).json({ 71 | error: true, 72 | message: error.message, 73 | request: {shop, gql, variables} 74 | }) 75 | 76 | } finally { 77 | await client.close() 78 | } 79 | 80 | // If you reached this everything is all apart 81 | // Status 418 - I am a teapot. 82 | } 83 | 84 | export default verifiedConnection(query) -------------------------------------------------------------------------------- /pages/api/verifybilling.ts: -------------------------------------------------------------------------------- 1 | import {MongoClient} from 'mongodb' 2 | import { findOneStoreDocumentById, updateField, createDBClient } from '../../_utils/atlasMethods'; 3 | import { getCurrentAppBilling } from '../../_utils/shopifyMethods'; 4 | import dataShapeBillingVerify from '../../_utils/dataShapers/dataShapeBillingVerify'; 5 | import verifiedConnection from '../../_middleware/verifiedConnection'; 6 | import { NextApiRequest, NextApiResponse } from 'next'; 7 | 8 | const verifiyBillingHandler = async (req: NextApiRequest, res: NextApiResponse) => { 9 | 10 | // no body sent 11 | if(!req.query) { 12 | return res.status(400).json({error: true, message: 'No request submitted for handling.'}) 13 | } 14 | 15 | // destructure request body 16 | const {shop, charge_id, cak} = req.query 17 | 18 | 19 | // // Validate Incoming 20 | if(!shop || !cak) { 21 | return res.status(400).json({error: true, message: 'Missing or incorrect parameters supplied'}) 22 | } 23 | 24 | const client = createDBClient() 25 | 26 | try { 27 | await client.connect() 28 | 29 | const dbDoc = await findOneStoreDocumentById(client, `${shop}`) 30 | 31 | // checks for existence authenticity of pass through 32 | if(!dbDoc || dbDoc.callAuthenticityKey !== cak) { 33 | res.writeHead(302, { 34 | 'Location': `https://${shop}/admin/apps/${process.env.SHOPIFY_API_KEY}/billing?errorVerifying=true` 35 | }) 36 | return res.end() 37 | } 38 | 39 | const shopifyCurrentAppBilling = await getCurrentAppBilling(shop, dbDoc.shopifyApiToken) 40 | 41 | if(!shopifyCurrentAppBilling) { 42 | res.writeHead(302, { 43 | 'Location': `https://${shop}/admin/apps/${process.env.SHOPIFY_API_KEY}/billing?errorVerifying=true` 44 | }) 45 | return res.end() 46 | } 47 | 48 | 49 | 50 | const specificBillingObjectApproved = shopifyCurrentAppBilling[0] 51 | const shapedBilling = dataShapeBillingVerify(specificBillingObjectApproved) 52 | const updateDb = await updateField(client, `${shop}`, 'billing', shapedBilling) 53 | 54 | if(updateDb) { 55 | res.writeHead(302, { 56 | 'Location': `https://${shop}/admin/apps/${process.env.SHOPIFY_API_KEY}/dashboard?billingApproved=true&plan=${shapedBilling.tier}` 57 | }) 58 | 59 | return res.end() 60 | } 61 | 62 | } catch (error) { 63 | 64 | res.writeHead(300, { 65 | 'Location': `https://${shop}/admin/apps/${process.env.SHOPIFY_API_KEY}/billing?errorVerifying=true` 66 | }) 67 | return res.end() 68 | 69 | } finally { 70 | await client.close() 71 | } 72 | 73 | res.writeHead(302, { 74 | 'Location': `https://${shop}/admin/apps/${process.env.SHOPIFY_API_KEY}/billing` 75 | }) 76 | return res.end() 77 | } 78 | 79 | export default verifiedConnection(verifiyBillingHandler) -------------------------------------------------------------------------------- /pages/api/webhooks/customers/data_request.ts: -------------------------------------------------------------------------------- 1 | import {checkWebhookHmacValidity, createRawBody} from 'shopify-hmac-validation' 2 | import validateWebhook from '../../../../_utils/validateWebhook' 3 | import { NextApiRequest, NextApiResponse } from 'next' 4 | 5 | export default async (req: NextApiRequest, res: NextApiResponse) => { 6 | 7 | if(req.method !== 'POST'){ 8 | return res.status(429).json({ 9 | body: 'Unauthorized access' 10 | }) 11 | } 12 | 13 | 14 | const hmac: string | string[] = req.headers['x-shopify-hmac-sha256'] 15 | const shop = req.headers['x-shopify-shop-domain'] 16 | 17 | if(!shop || !hmac) { 18 | return res.status(429).json({ 19 | body: 'Request could not be completed' 20 | }) 21 | } 22 | 23 | 24 | if(validateWebhook(req, hmac)) { 25 | console.log('webhook Valid') 26 | // If you handle customer data, remoember to handle its deletion here. 27 | // the boilerplate does not so responding. 28 | // ... Do stuff 29 | 30 | 31 | 32 | // ... Finish doing stuff 33 | return res.status(200).json({ 34 | body: `This app does not store customer data on ${shop}` 35 | }) 36 | 37 | 38 | } else { 39 | 40 | console.error({error: true, req: {headers: req.headers, body: req.body, shop }}) 41 | // custom logging if you so choose 42 | return res.status(429).json({ 43 | body: 'Request is not validated by HMAC' 44 | }) 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /pages/api/webhooks/customers/redact.ts: -------------------------------------------------------------------------------- 1 | 2 | import validateWebhook from '../../../../_utils/validateWebhook' 3 | 4 | export default async (req, res) => { 5 | 6 | if(req.method !== 'POST'){ 7 | return res.status(429).json({ 8 | body: 'Unauthorized access' 9 | }) 10 | } 11 | 12 | 13 | const hmac = req.headers['x-shopify-hmac-sha256'] 14 | const shop = req.headers['x-shopify-shop-domain'] 15 | 16 | if(!shop || !hmac) { 17 | return res.status(429).json({ 18 | body: 'Request could not be completed' 19 | }) 20 | } 21 | 22 | 23 | if(validateWebhook(req, hmac)) { 24 | console.log('webhook Valid') 25 | // If you handle customer data, remoember to handle its deletion here. 26 | // the boilerplate does not so responding. 27 | // ... Do stuff 28 | 29 | 30 | 31 | // ... Finish doing stuff 32 | return res.status(200).json({ 33 | body: `This app does not store customer data on ${shop}` 34 | }) 35 | 36 | 37 | } else { 38 | 39 | console.error({error: true, req: {headers: req.headers, body: req.body, shop }}) 40 | // custom logging if you so choose 41 | return res.status(429).json({ 42 | body: 'Request is not validated by HMAC' 43 | }) 44 | } 45 | } -------------------------------------------------------------------------------- /pages/api/webhooks/shop/redact.ts: -------------------------------------------------------------------------------- 1 | import { deleteDocumentById, createDBClient } from '../../../../_utils/atlasMethods' 2 | import { MongoClient } from 'mongodb' 3 | import validateWebhook from '../../../../_utils/validateWebhook' 4 | 5 | export default async (req, res) => { 6 | 7 | if(req.method !== 'POST'){ 8 | return res.status(429).json({ 9 | body: 'Unauthorized access' 10 | }) 11 | } 12 | 13 | // headers 14 | const hmac = req.headers['x-shopify-hmac-sha256'] 15 | const shop = req.headers['x-shopify-shop-domain'] 16 | 17 | const {shop_domain} = req.body 18 | 19 | if(!shop || !hmac || shop_domain !== shop) { 20 | return res.status(429).json({ 21 | body: 'Request could not be completed' 22 | }) 23 | } 24 | 25 | 26 | if(!validateWebhook(req, hmac)) { 27 | console.error({error: true, req: {headers: req.headers, body: req.body, shop }}) 28 | // custom logging if you so choose 29 | return res.status(429).json({ 30 | body: 'Request is not validated by HMAC' 31 | }) 32 | } 33 | 34 | // All good continue and remove data 35 | const client = createDBClient() 36 | 37 | try { 38 | // Connect to the MongoDB cluster 39 | await client.connect(); 40 | // delete the doc 41 | 42 | //... DO STUFF 43 | 44 | const result = await deleteDocumentById(client, shop_domain) 45 | 46 | if(result) { 47 | return res.status(200).json({ 48 | body: `Shop: ${shop_domain} deleted sucessfully` 49 | }) 50 | } 51 | 52 | return res.status(500).json({ 53 | body: `Shop: ${shop_domain} not deleted, or could not be found` 54 | }) 55 | 56 | } catch (error) { 57 | 58 | return res.status(500).json({ 59 | body: { 60 | message: `Shop: ${shop_domain} not deleted, or could not be found`, 61 | errorMessage: error.message 62 | } 63 | }) 64 | 65 | } finally { 66 | await client.close() 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /pages/billing.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Stage from '../components/Stage' 3 | 4 | import BillingSelector from '../components/billing/BillingSelector'; 5 | 6 | const Billing = () => { 7 | 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } 14 | 15 | export default Billing -------------------------------------------------------------------------------- /pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useRouter } from 'next/router' 3 | import Stage from '../components/Stage' 4 | import { Card, Banner } from '@shopify/polaris' 5 | import { T } from '../components/TextProvider'; 6 | 7 | const Dashboard = () => { 8 | const router = useRouter() 9 | 10 | const {billingApproved, plan} = router.query 11 | 12 | 13 | 14 | return ( 15 | 16 | {billingApproved && {}} 21 | > 22 |

If you would like a quick tour of the features

23 |
24 | } 25 | 28 | {T('Test')} 29 |

Place holder for the dashboard content!

30 |
31 |
32 | ) 33 | } 34 | 35 | export default Dashboard -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react' 2 | import Head from 'next/head' 3 | import qs from 'query-string' 4 | import axios from 'axios' 5 | import LoadingPage from '../components/global/LoadingPage' 6 | import useAppBridge from '../hooks/useAppBridge' 7 | 8 | // probably not needed, will tie these into call back auth flow down the line. 9 | const nonce = require('nonce')() 10 | const state = nonce() 11 | 12 | 13 | const Home = (props) => { 14 | 15 | 16 | // page context params 17 | useEffect(() => { 18 | if(typeof window !== 'undefined' && window.location) { 19 | const query = qs.parse(window.location.search) 20 | axios.post('/api/auth', { 21 | query: query, 22 | state: state 23 | }) 24 | .then(response => { 25 | 26 | if(response.data.redirectTo) { 27 | console.log('Redirecting with scopes', response.data.redirectTo) 28 | if(window.parent){ 29 | window.parent.location.href = response.data.redirectTo 30 | } else { 31 | window.location.href = response.data.redirectTo 32 | } 33 | } 34 | 35 | }) 36 | .catch(function (error) { 37 | // handle error 38 | console.error(error); 39 | }) 40 | } 41 | }, []) 42 | 43 | 44 | return () 45 | } 46 | 47 | export default Home -------------------------------------------------------------------------------- /pages/settings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Stage from '../components/Stage' 3 | import { Card } from '@shopify/polaris' 4 | 5 | 6 | const Settings = () => { 7 | 8 | return ( 9 | 10 | 13 |

Place holder for the settings content!

14 |
15 |
16 | ) 17 | } 18 | 19 | export default Settings -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leighs-hammer/shopify-app-boilerplate-nextjs-redux-nosql/98948bcee32b9ee418b931baf02dafee4db96795/public/favicon.ico -------------------------------------------------------------------------------- /public/locales/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "Polaris": { 3 | "Avatar": { 4 | "label": "Avatar", 5 | "labelWithInitials": "Avatar s iniciály {initials}" 6 | }, 7 | "Autocomplete": { 8 | "spinnerAccessibilityLabel": "Načítání" 9 | }, 10 | "Badge": { 11 | "PROGRESS_LABELS": { 12 | "incomplete": "Nedokončeno", 13 | "partiallyComplete": "Částečně dokončeno", 14 | "complete": "Dokončen" 15 | }, 16 | "STATUS_LABELS": { 17 | "info": "Informace", 18 | "success": "Zdařilo se", 19 | "warning": "Varování", 20 | "attention": "Upozornění", 21 | "new": "Nové" 22 | } 23 | }, 24 | "Button": { 25 | "spinnerAccessibilityLabel": "Načítání", 26 | "connectedDisclosureAccessibilityLabel": "Související akce" 27 | }, 28 | "Common": { 29 | "checkbox": "zaškrtávací políčko", 30 | "undo": "Vrátit zpět", 31 | "cancel": "Zrušit", 32 | "newWindowAccessibilityHint": "(otevře nové okno)", 33 | "clear": "Vymazat", 34 | "close": "Zavřít", 35 | "submit": "Odeslat", 36 | "more": "Více" 37 | }, 38 | "ContextualSaveBar": { 39 | "save": "Uložit", 40 | "discard": "Zahodit" 41 | }, 42 | "DataTable": { 43 | "sortAccessibilityLabel": "seřadit {direction} podle", 44 | "navAccessibilityLabel": "Posunout tabulku o jeden sloupec {direction}", 45 | "totalsRowHeading": "Celkem", 46 | "totalRowHeading": "Celkem" 47 | }, 48 | "DatePicker": { 49 | "previousMonth": "Zobrazit předchozí měsíc, {previousMonthName} {showPreviousYear}", 50 | "nextMonth": "Zobrazit příští měsíc, {nextMonth} {nextYear}", 51 | "today": "Dnes ", 52 | "months": { 53 | "january": "Leden", 54 | "february": "Únor", 55 | "march": "Březen", 56 | "april": "Duben", 57 | "may": "Květen", 58 | "june": "Červen", 59 | "july": "Červenec", 60 | "august": "Srpen", 61 | "september": "Září", 62 | "october": "Říjen", 63 | "november": "Listopad", 64 | "december": "Prosinec" 65 | }, 66 | "daysAbbreviated": { 67 | "monday": "Po", 68 | "tuesday": "Út", 69 | "wednesday": "St", 70 | "thursday": "Čt", 71 | "friday": "Pá", 72 | "saturday": "So", 73 | "sunday": "Ne" 74 | } 75 | }, 76 | "DiscardConfirmationModal": { 77 | "title": "Zahodit všechny neuložené změny", 78 | "message": "Pokud zahodíte změny, smažete všechny úpravy provedené od posledního uložení.", 79 | "primaryAction": "Zahodit změny", 80 | "secondaryAction": "Pokračovat v úpravách" 81 | }, 82 | "DropZone": { 83 | "overlayTextFile": "Soubor nahrajete přetáhnutím", 84 | "overlayTextImage": "Obrázek nahrajete přetáhnutím", 85 | "errorOverlayTextFile": "Typ souboru není platný", 86 | "errorOverlayTextImage": "Typ obrázku není platný", 87 | "FileUpload": { 88 | "actionTitleFile": "Přidat soubor", 89 | "actionTitleImage": "Přidat obrázek", 90 | "actionHintFile": "nebo nahrajte soubor přetáhnutím", 91 | "actionHintImage": "nebo nahrajte obrázek přetáhnutím", 92 | "label": "Nahrát soubor" 93 | } 94 | }, 95 | "EmptySearchResult": { 96 | "altText": "Žádné výsledky hledání" 97 | }, 98 | "Frame": { 99 | "skipToContent": "Přeskočit na obsah", 100 | "Navigation": { 101 | "closeMobileNavigationLabel": "Zavřít navigaci" 102 | } 103 | }, 104 | "Icon": { 105 | "backdropWarning": "Ikona {color} nepoužívá pozadí. Barvy ikon, které mají pozadí: {colorsWithBackDrops}" 106 | }, 107 | "ActionMenu": { 108 | "RollupActions": { 109 | "rollupButton": "Akce" 110 | } 111 | }, 112 | "Filters": { 113 | "moreFilters": "Další filtry", 114 | "filter": "Filtr: {resourceName}", 115 | "noFiltersApplied": "Nejsou použity žádné filtry", 116 | "cancel": "Zrušit", 117 | "done": "Hotovo", 118 | "clearAllFilters": "Vymazat všechny filtry", 119 | "clear": "Vymazat", 120 | "clearLabel": "Vymazat {filterName}", 121 | "moreFiltersWithCount": "Další filtry ({count})" 122 | }, 123 | "Modal": { 124 | "iFrameTitle": "značka těla", 125 | "modalWarning": "V modálním parametru chybí následující povinné vlastnosti: {missingProps}" 126 | }, 127 | "Pagination": { 128 | "previous": "Předchozí", 129 | "next": "Další", 130 | "pagination": "Stránkování" 131 | }, 132 | "ProgressBar": { 133 | "negativeWarningMessage": "Hodnoty předané dál by neměly být záporné. {progress}: vynulování", 134 | "exceedWarningMessage": "Hodnoty předané dál by neměly být vyšší než 100. {progress}: nastavení na hodnotu 100" 135 | }, 136 | "ResourceList": { 137 | "sortingLabel": "Seřadit podle", 138 | "defaultItemSingular": "položka", 139 | "defaultItemPlural": "položek", 140 | "showing": "Zobrazení: {itemsCount} {resource}", 141 | "loading": "Načítání: {resource}", 142 | "selected": "Výběr: {selectedItemsCount}", 143 | "allItemsSelected": "V obchodě bylo vybráno veškeré zboží typu {resourceNamePlural} o délce {itemsLength}+.", 144 | "selectAllItems": "Vybrat v obchodě všechno zboží typu {resourceNamePlural} o délce {itemsLength}+", 145 | "emptySearchResultTitle": "{resourceNamePlural} se nepodařilo nalézt", 146 | "emptySearchResultDescription": "Zkuste změnit filtry nebo hledaný výraz", 147 | "selectButtonText": "Vybrat", 148 | "a11yCheckboxDeselectAllSingle": "Zrušit výběr: {resourceNameSingular}", 149 | "a11yCheckboxSelectAllSingle": "Vybrat {resourceNameSingular}", 150 | "a11yCheckboxDeselectAllMultiple": "Zrušit celý výběr: {itemsLength} {resourceNamePlural}", 151 | "a11yCheckboxSelectAllMultiple": "Vybrat vše: {itemsLength} {resourceNamePlural}", 152 | "ariaLiveSingular": "{itemsLength} položka", 153 | "ariaLivePlural": "{itemsLength} položek", 154 | "Item": { 155 | "actionsDropdownLabel": "Akce pro: {accessibilityLabel}", 156 | "actionsDropdown": "Rozbalovací nabídka akcí", 157 | "viewItem": "Zobrazit podrobnosti pro: {itemName}" 158 | }, 159 | "BulkActions": { 160 | "actionsActivatorLabel": "Akce", 161 | "moreActionsActivatorLabel": "Další akce", 162 | "warningMessage": "Za účelem zlepšení uživatelské zkušenosti. Promo akcí by mělo být maximálně {maxPromotedActions}." 163 | }, 164 | "FilterCreator": { 165 | "filterButtonLabel": "Filtr", 166 | "selectFilterKeyPlaceholder": "Vyberte filtr...", 167 | "addFilterButtonLabel": "Přidat filtr", 168 | "showAllWhere": "Zobrazit všechny případy ({resourceNamePlural}), kde:" 169 | }, 170 | "FilterControl": { 171 | "textFieldLabel": "Prohledejte {resourceNamePlural}" 172 | }, 173 | "FilterValueSelector": { 174 | "selectFilterValuePlaceholder": "Vyberte filtr..." 175 | }, 176 | "DateSelector": { 177 | "dateFilterLabel": "Vyberte hodnotu", 178 | "dateValueLabel": "Datum", 179 | "dateValueError": "Použijte formát RRRR-MM-DD", 180 | "dateValuePlaceholder": "RRRR-MM-DD", 181 | "SelectOptions": { 182 | "PastWeek": "za poslední týden", 183 | "PastMonth": "za poslední měsíc", 184 | "PastQuarter": "za poslední tři měsíce", 185 | "PastYear": "za poslední rok", 186 | "ComingWeek": "příští týden", 187 | "ComingMonth": "příští měsíc", 188 | "ComingQuarter": "v příštích třech měsících", 189 | "ComingYear": "v příštím roce", 190 | "OnOrBefore": "dne nebo dříve než", 191 | "OnOrAfter": "dne nebo později než" 192 | }, 193 | "FilterLabelForValue": { 194 | "past_week": "za poslední týden", 195 | "past_month": "za poslední měsíc", 196 | "past_quarter": "za poslední tři měsíce", 197 | "past_year": "za poslední rok", 198 | "coming_week": "příští týden", 199 | "coming_month": "příští měsíc", 200 | "coming_quarter": "v příštích třech měsících", 201 | "coming_year": "v příštím roce", 202 | "on_or_before": "před {date}", 203 | "on_or_after": "po {date}" 204 | } 205 | }, 206 | "showingTotalCount": "Zobrazuje se {itemsCount} z {totalItemsCount} {resource}" 207 | }, 208 | "SkeletonPage": { 209 | "loadingLabel": "Načítání stránky" 210 | }, 211 | "Spinner": { 212 | "warningMessage": "Barva ({color}) není určena pro spinnery o velikosti: {size}). Pro velké spinnery jsou k dispozici tyto barvy: {colors}" 213 | }, 214 | "Tabs": { 215 | "toggleTabsLabel": "Více karet" 216 | }, 217 | "Tag": { 218 | "ariaLabel": "Odstranit {children}" 219 | }, 220 | "TextField": { 221 | "characterCount": "Počet znaků: {count}", 222 | "characterCountWithMaxLength": "Použito {count} z {limit} znaků" 223 | }, 224 | "TopBar": { 225 | "toggleMenuLabel": "Přepnout nabídku", 226 | "SearchField": { 227 | "clearButtonLabel": "Vymazat", 228 | "search": "Hledat" 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/locales/da.json: -------------------------------------------------------------------------------- 1 | { 2 | "Polaris": { 3 | "Avatar": { 4 | "label": "Avatar", 5 | "labelWithInitials": "Avatar med initialer {initials}" 6 | }, 7 | "Autocomplete": { 8 | "spinnerAccessibilityLabel": "Indlæser" 9 | }, 10 | "Badge": { 11 | "PROGRESS_LABELS": { 12 | "incomplete": "Ufuldstændig", 13 | "partiallyComplete": "Delvist fuldført", 14 | "complete": "Fuldført" 15 | }, 16 | "STATUS_LABELS": { 17 | "info": "Info", 18 | "success": "Succes", 19 | "warning": "Advarsel", 20 | "attention": "Bemærk", 21 | "new": "Nye" 22 | } 23 | }, 24 | "Button": { 25 | "spinnerAccessibilityLabel": "Indlæser", 26 | "connectedDisclosureAccessibilityLabel": "Relaterede handlinger" 27 | }, 28 | "Common": { 29 | "checkbox": "afkrydsningsfelt", 30 | "undo": "Fortryd", 31 | "cancel": "Annullér", 32 | "newWindowAccessibilityHint": "(åbner et nyt vindue)", 33 | "clear": "Ryd", 34 | "close": "Luk", 35 | "submit": "Indsend", 36 | "more": "Mere" 37 | }, 38 | "ContextualSaveBar": { 39 | "save": "Gem", 40 | "discard": "Fortryd" 41 | }, 42 | "DataTable": { 43 | "sortAccessibilityLabel": "sortér {direction} efter", 44 | "navAccessibilityLabel": "Rul tabel én kolonne mod {direction}", 45 | "totalsRowHeading": "I alt", 46 | "totalRowHeading": "I alt" 47 | }, 48 | "DatePicker": { 49 | "previousMonth": "Vis tidligere måned, {previousMonthName} {showPreviousYear}", 50 | "nextMonth": "Vis næste måned, {nextMonth} {nextYear}", 51 | "today": "I dag ", 52 | "months": { 53 | "january": "Januar", 54 | "february": "Februar", 55 | "march": "Marts", 56 | "april": "April", 57 | "may": "Maj", 58 | "june": "Juni", 59 | "july": "Juli", 60 | "august": "August", 61 | "september": "September", 62 | "october": "Oktober", 63 | "november": "November", 64 | "december": "December" 65 | }, 66 | "daysAbbreviated": { 67 | "monday": "Ma", 68 | "tuesday": "Ti", 69 | "wednesday": "On", 70 | "thursday": "To", 71 | "friday": "Fr", 72 | "saturday": "Lø", 73 | "sunday": "Sø" 74 | } 75 | }, 76 | "DiscardConfirmationModal": { 77 | "title": "Fortryd alle ændringer, der ikke er gemt", 78 | "message": "Hvis du fortryder ændringer, sletter du alle ændringer, du har foretaget, siden du gemte sidste gang.", 79 | "primaryAction": "Fortryd ændringer", 80 | "secondaryAction": "Fortsæt redigering" 81 | }, 82 | "DropZone": { 83 | "overlayTextFile": "Slip fil for at uploade", 84 | "overlayTextImage": "Slip billede for at uploade", 85 | "errorOverlayTextFile": "Filtype er ikke gyldig", 86 | "errorOverlayTextImage": "Billedtype er ikke gyldig", 87 | "FileUpload": { 88 | "actionTitleFile": "Tilføj fil", 89 | "actionTitleImage": "Tilføj billede", 90 | "actionHintFile": "eller slip filer for at uploade", 91 | "actionHintImage": "eller slip billeder for at uploade", 92 | "label": "Upload fil" 93 | } 94 | }, 95 | "EmptySearchResult": { 96 | "altText": "Tøm søgeresultater" 97 | }, 98 | "Frame": { 99 | "skipToContent": "Skip til indhold", 100 | "Navigation": { 101 | "closeMobileNavigationLabel": "Luk navigation" 102 | } 103 | }, 104 | "Icon": { 105 | "backdropWarning": "{color}-ikonet accepterer ikke baggrunde. De ikonfarver, der har baggrunde, er: {colorsWithBackDrops}" 106 | }, 107 | "ActionMenu": { 108 | "RollupActions": { 109 | "rollupButton": "Handlinger" 110 | } 111 | }, 112 | "Filters": { 113 | "moreFilters": "Flere filtre", 114 | "filter": "Filtrer {resourceName}", 115 | "noFiltersApplied": "Der blev ikke anvendt nogen filtre", 116 | "cancel": "Annuller", 117 | "done": "Udført", 118 | "clearAllFilters": "Ryd alle filtre", 119 | "clear": "Ryd", 120 | "clearLabel": "Ryd {filterName}", 121 | "moreFiltersWithCount": "Flere filtre: ({count})" 122 | }, 123 | "Modal": { 124 | "iFrameTitle": "markering af brødtekst", 125 | "modalWarning": "Disse krævede egenskaber mangler fra modus: {missingProps}" 126 | }, 127 | "Pagination": { 128 | "previous": "Forrige", 129 | "next": "Næste", 130 | "pagination": "Sidenummerering" 131 | }, 132 | "ProgressBar": { 133 | "negativeWarningMessage": "Værdier, der overføres til statusegenskab, må ikke være negative. Angiver {progress} til 0.", 134 | "exceedWarningMessage": "Værdier, der overføres til statusegenskab, må ikke overstige 100. Indstiller {progress} til 100." 135 | }, 136 | "ResourceList": { 137 | "sortingLabel": "Sortér efter", 138 | "defaultItemSingular": "vare", 139 | "defaultItemPlural": "varer", 140 | "showing": "Viser {itemsCount} {resource}", 141 | "loading": "Indlæser {resource}", 142 | "selected": "{selectedItemsCount} valgt", 143 | "allItemsSelected": "Al {itemsLength} + {resourceNamePlural} i din butik er valgt.", 144 | "selectAllItems": "Vælg alle {itemsLength} + {resourceNamePlural} i butikken", 145 | "emptySearchResultTitle": "Ingen {resourceNamePlural} blev fundet", 146 | "emptySearchResultDescription": "Prøv et andet filter eller søgeord", 147 | "selectButtonText": "Vælg", 148 | "a11yCheckboxDeselectAllSingle": "Fravælg {resourceNameSingular}", 149 | "a11yCheckboxSelectAllSingle": "Vælg {resourceNameSingular}", 150 | "a11yCheckboxDeselectAllMultiple": "Fravælg alle {itemsLength} {resourceNamePlural}", 151 | "a11yCheckboxSelectAllMultiple": "Vælg alle {itemsLength} {resourceNamePlural}", 152 | "ariaLiveSingular": "{itemsLength} vare", 153 | "ariaLivePlural": "{itemsLength} varer", 154 | "Item": { 155 | "actionsDropdownLabel": "Handlinger for {accessibilityLabel}", 156 | "actionsDropdown": "Rulleliste med handlinger", 157 | "viewItem": "Se detaljer for {itemName}" 158 | }, 159 | "BulkActions": { 160 | "actionsActivatorLabel": "Handlinger", 161 | "moreActionsActivatorLabel": "Flere handlinger", 162 | "warningMessage": "Sikring af en bedre brugeroplevelse. Der bør maksimalt være {maxPromotedActions} handlinger, der promoveres." 163 | }, 164 | "FilterCreator": { 165 | "filterButtonLabel": "Filtrer", 166 | "selectFilterKeyPlaceholder": "Vælg et filter ...", 167 | "addFilterButtonLabel": "Tilføj filter", 168 | "showAllWhere": "Vis alle {resourceNamePlural}, hvor:" 169 | }, 170 | "FilterControl": { 171 | "textFieldLabel": "Søg efter {resourceNamePlural}" 172 | }, 173 | "FilterValueSelector": { 174 | "selectFilterValuePlaceholder": "Vælg et filter ..." 175 | }, 176 | "DateSelector": { 177 | "dateFilterLabel": "Vælg en værdi", 178 | "dateValueLabel": "Dato", 179 | "dateValueError": "Match ÅÅÅÅ-MM-DD-format", 180 | "dateValuePlaceholder": "ÅÅÅÅ-MM-DD", 181 | "SelectOptions": { 182 | "PastWeek": "i den sidste uge", 183 | "PastMonth": "i den sidste måned", 184 | "PastQuarter": "i de sidste 3 måneder", 185 | "PastYear": "i det sidste år", 186 | "ComingWeek": "næste uge", 187 | "ComingMonth": "næste måned", 188 | "ComingQuarter": "i de næste 3 måneder", 189 | "ComingYear": "i det næste år", 190 | "OnOrBefore": "på eller før", 191 | "OnOrAfter": "på eller efter" 192 | }, 193 | "FilterLabelForValue": { 194 | "past_week": "i den sidste uge", 195 | "past_month": "i den sidste måned", 196 | "past_quarter": "i de sidste 3 måneder", 197 | "past_year": "i det sidste år", 198 | "coming_week": "næste uge", 199 | "coming_month": "næste måned", 200 | "coming_quarter": "i de næste 3 måneder", 201 | "coming_year": "i det næste år", 202 | "on_or_before": "før {date}", 203 | "on_or_after": "efter {date}" 204 | } 205 | }, 206 | "showingTotalCount": "Viser {itemsCount} af {totalItemsCount} {resource}" 207 | }, 208 | "SkeletonPage": { 209 | "loadingLabel": "Indlæser side" 210 | }, 211 | "Spinner": { 212 | "warningMessage": "Farven {color} er ikke beregnet til at blive anvendt til {size} spinnere. Tilgængelige farver på store spinnere er: {colors}" 213 | }, 214 | "Tabs": { 215 | "toggleTabsLabel": "Flere faner" 216 | }, 217 | "Tag": { 218 | "ariaLabel": "Fjern {children}" 219 | }, 220 | "TextField": { 221 | "characterCount": "{count} tegn", 222 | "characterCountWithMaxLength": "{count} af {limit} benyttede tegn" 223 | }, 224 | "TopBar": { 225 | "toggleMenuLabel": "Slå menu til/fra", 226 | "SearchField": { 227 | "clearButtonLabel": "Ryd", 228 | "search": "Søg" 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Test": "I exist on EN", 3 | "Polaris": { 4 | "Avatar": { 5 | "label": "Avatar", 6 | "labelWithInitials": "Avatar with initials {initials}" 7 | }, 8 | 9 | "Autocomplete": { 10 | "spinnerAccessibilityLabel": "Loading" 11 | }, 12 | 13 | "Badge": { 14 | "PROGRESS_LABELS": { 15 | "incomplete": "Incomplete", 16 | "partiallyComplete": "Partially complete", 17 | "complete": "Complete" 18 | }, 19 | "STATUS_LABELS": { 20 | "info": "Info", 21 | "success": "Success", 22 | "warning": "Warning", 23 | "attention": "Attention", 24 | "new": "New" 25 | } 26 | }, 27 | 28 | "Button": { 29 | "spinnerAccessibilityLabel": "Loading", 30 | "connectedDisclosureAccessibilityLabel": "Related actions" 31 | }, 32 | 33 | "Common": { 34 | "checkbox": "checkbox", 35 | "undo": "Undo", 36 | "cancel": "Cancel", 37 | "newWindowAccessibilityHint": "(opens a new window)", 38 | "clear": "Clear", 39 | "close": "Close", 40 | "submit": "Submit", 41 | "more": "More" 42 | }, 43 | 44 | "ContextualSaveBar": { 45 | "save": "Save", 46 | "discard": "Discard" 47 | }, 48 | 49 | "DataTable": { 50 | "sortAccessibilityLabel": "sort {direction} by", 51 | "navAccessibilityLabel": "Scroll table {direction} one column", 52 | "totalsRowHeading": "Totals", 53 | "totalRowHeading": "Total" 54 | }, 55 | 56 | "DatePicker": { 57 | "previousMonth": "Show previous month, {previousMonthName} {showPreviousYear}", 58 | "nextMonth": "Show next month, {nextMonth} {nextYear}", 59 | "today": "Today ", 60 | "months": { 61 | "january": "January", 62 | "february": "February", 63 | "march": "March", 64 | "april": "April", 65 | "may": "May", 66 | "june": "June", 67 | "july": "July", 68 | "august": "August", 69 | "september": "September", 70 | "october": "October", 71 | "november": "November", 72 | "december": "December" 73 | }, 74 | "daysAbbreviated": { 75 | "monday": "Mo", 76 | "tuesday": "Tu", 77 | "wednesday": "We", 78 | "thursday": "Th", 79 | "friday": "Fr", 80 | "saturday": "Sa", 81 | "sunday": "Su" 82 | } 83 | }, 84 | 85 | "DiscardConfirmationModal": { 86 | "title": "Discard all unsaved changes", 87 | "message": "If you discard changes, you’ll delete any edits you made since you last saved.", 88 | "primaryAction": "Discard changes", 89 | "secondaryAction": "Continue editing" 90 | }, 91 | 92 | "DropZone": { 93 | "overlayTextFile": "Drop file to upload", 94 | "overlayTextImage": "Drop image to upload", 95 | "errorOverlayTextFile": "File type is not valid", 96 | "errorOverlayTextImage": "Image type is not valid", 97 | 98 | "FileUpload": { 99 | "actionTitleFile": "Add file", 100 | "actionTitleImage": "Add image", 101 | "actionHintFile": "or drop files to upload", 102 | "actionHintImage": "or drop images to upload", 103 | "label": "Upload file" 104 | } 105 | }, 106 | 107 | "EmptySearchResult": { 108 | "altText": "Empty search results" 109 | }, 110 | 111 | "Frame": { 112 | "skipToContent": "Skip to content", 113 | "Navigation": { 114 | "closeMobileNavigationLabel": "Close navigation" 115 | } 116 | }, 117 | 118 | "Icon": { 119 | "backdropWarning": "The {color} icon doesn’t accept backdrops. The icon colors that have backdrops are: {colorsWithBackDrops}" 120 | }, 121 | 122 | "ActionMenu": { 123 | "RollupActions": { 124 | "rollupButton": "Actions" 125 | } 126 | }, 127 | 128 | "Filters": { 129 | "moreFilters": "More filters", 130 | "moreFiltersWithCount": "More filters ({count})", 131 | "filter": "Filter {resourceName}", 132 | "noFiltersApplied": "No filters applied", 133 | "cancel": "Cancel", 134 | "done": "Done", 135 | "clearAllFilters": "Clear all filters", 136 | "clear": "Clear", 137 | "clearLabel": "Clear {filterName}" 138 | }, 139 | 140 | "Modal": { 141 | "iFrameTitle": "body markup", 142 | "modalWarning": "These required properties are missing from Modal: {missingProps}" 143 | }, 144 | 145 | "Pagination": { 146 | "previous": "Previous", 147 | "next": "Next", 148 | "pagination": "Pagination" 149 | }, 150 | 151 | "ProgressBar": { 152 | "negativeWarningMessage": "Values passed to the progress prop shouldn’t be negative. Resetting {progress} to 0.", 153 | "exceedWarningMessage": "Values passed to the progress prop shouldn’t exceed 100. Setting {progress} to 100." 154 | }, 155 | 156 | "ResourceList": { 157 | "sortingLabel": "Sort by", 158 | "defaultItemSingular": "item", 159 | "defaultItemPlural": "items", 160 | "showing": "Showing {itemsCount} {resource}", 161 | "showingTotalCount": "Showing {itemsCount} of {totalItemsCount} {resource}", 162 | "loading": "Loading {resource}", 163 | "selected": "{selectedItemsCount} selected", 164 | "allItemsSelected": "All {itemsLength}+ {resourceNamePlural} in your store are selected.", 165 | "selectAllItems": "Select all {itemsLength}+ {resourceNamePlural} in your store", 166 | "emptySearchResultTitle": "No {resourceNamePlural} found", 167 | "emptySearchResultDescription": "Try changing the filters or search term", 168 | "selectButtonText": "Select", 169 | "a11yCheckboxDeselectAllSingle": "Deselect {resourceNameSingular}", 170 | "a11yCheckboxSelectAllSingle": "Select {resourceNameSingular}", 171 | "a11yCheckboxDeselectAllMultiple": "Deselect all {itemsLength} {resourceNamePlural}", 172 | "a11yCheckboxSelectAllMultiple": "Select all {itemsLength} {resourceNamePlural}", 173 | "ariaLiveSingular": "{itemsLength} item", 174 | "ariaLivePlural": "{itemsLength} items", 175 | 176 | "Item": { 177 | "actionsDropdownLabel": "Actions for {accessibilityLabel}", 178 | "actionsDropdown": "Actions dropdown", 179 | "viewItem": "View details for {itemName}" 180 | }, 181 | 182 | "BulkActions": { 183 | "actionsActivatorLabel": "Actions", 184 | "moreActionsActivatorLabel": "More actions", 185 | "warningMessage": "To provide a better user experience. There should only be a maximum of {maxPromotedActions} promoted actions." 186 | }, 187 | 188 | "FilterCreator": { 189 | "filterButtonLabel": "Filter", 190 | "selectFilterKeyPlaceholder": "Select a filter\u2026", 191 | "addFilterButtonLabel": "Add filter", 192 | "showAllWhere": "Show all {resourceNamePlural} where:" 193 | }, 194 | 195 | "FilterControl": { 196 | "textFieldLabel": "Search {resourceNamePlural}" 197 | }, 198 | 199 | "FilterValueSelector": { 200 | "selectFilterValuePlaceholder": "Select a filter\u2026" 201 | }, 202 | 203 | "DateSelector": { 204 | "dateFilterLabel": "Select a value", 205 | "dateValueLabel": "Date", 206 | "dateValueError": "Match YYYY-MM-DD format", 207 | "dateValuePlaceholder": "YYYY-MM-DD", 208 | "SelectOptions": { 209 | "PastWeek": "in the last week", 210 | "PastMonth": "in the last month", 211 | "PastQuarter": "in the last 3 months", 212 | "PastYear": "in the last year", 213 | "ComingWeek": "next week", 214 | "ComingMonth": "next month", 215 | "ComingQuarter": "in the next 3 months", 216 | "ComingYear": "in the next year", 217 | "OnOrBefore": "on or before", 218 | "OnOrAfter": "on or after" 219 | }, 220 | "FilterLabelForValue": { 221 | "past_week": "in the last week", 222 | "past_month": "in the last month", 223 | "past_quarter": "in the last 3 months", 224 | "past_year": "in the last year", 225 | "coming_week": "next week", 226 | "coming_month": "next month", 227 | "coming_quarter": "in the next 3 months", 228 | "coming_year": "in the next year", 229 | "on_or_before": "before {date}", 230 | "on_or_after": "after {date}" 231 | } 232 | } 233 | }, 234 | 235 | "SkeletonPage": { 236 | "loadingLabel": "Page loading" 237 | }, 238 | 239 | "Spinner": { 240 | "warningMessage": "The color {color} is not meant to be used on {size} spinners. The colors available on large spinners are: {colors}" 241 | }, 242 | 243 | "Tabs": { 244 | "toggleTabsLabel": "More tabs" 245 | }, 246 | 247 | "Tag": { 248 | "ariaLabel": "Remove {children}" 249 | }, 250 | 251 | "TextField": { 252 | "characterCount": "{count} characters", 253 | "characterCountWithMaxLength": "{count} of {limit} characters used" 254 | }, 255 | 256 | "TopBar": { 257 | "toggleMenuLabel": "Toggle menu", 258 | 259 | "SearchField": { 260 | "clearButtonLabel": "Clear", 261 | "search": "Search" 262 | } 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /public/locales/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "Polaris": { 3 | "Avatar": { 4 | "label": "Avatar", 5 | "labelWithInitials": "Avatar alkukirjaimilla {initials}" 6 | }, 7 | "Autocomplete": { 8 | "spinnerAccessibilityLabel": "Ladataan" 9 | }, 10 | "Badge": { 11 | "PROGRESS_LABELS": { 12 | "incomplete": "Ei valmis", 13 | "partiallyComplete": "Osittain valmis", 14 | "complete": "Valmis" 15 | }, 16 | "STATUS_LABELS": { 17 | "info": "Tiedot", 18 | "success": "Onnistui", 19 | "warning": "Varoitus", 20 | "attention": "Huomio", 21 | "new": "Uusi" 22 | } 23 | }, 24 | "Button": { 25 | "spinnerAccessibilityLabel": "Ladataan", 26 | "connectedDisclosureAccessibilityLabel": "Aiheeseen liittyvät toimenpiteet" 27 | }, 28 | "Common": { 29 | "checkbox": "valintaruutu", 30 | "undo": "Kumoa", 31 | "cancel": "Peruuta", 32 | "newWindowAccessibilityHint": "(avaa uuden ikkunan)", 33 | "clear": "Tyhjennä", 34 | "close": "Sulje", 35 | "submit": "Lähetä", 36 | "more": "Lisää" 37 | }, 38 | "ContextualSaveBar": { 39 | "save": "Tallenna", 40 | "discard": "Hylkää" 41 | }, 42 | "DataTable": { 43 | "sortAccessibilityLabel": "lajittele {direction}:", 44 | "navAccessibilityLabel": "Selaa taulukkoa {direction} yksi sarake", 45 | "totalsRowHeading": "Yhteenlasketut", 46 | "totalRowHeading": "Yhteensä" 47 | }, 48 | "DatePicker": { 49 | "previousMonth": "Näytä edellinen kuukausi, {previousMonthName} {showPreviousYear}", 50 | "nextMonth": "Näytä seuraava kuukausi, {nextMonth} {nextYear}", 51 | "today": "Tänään ", 52 | "months": { 53 | "january": "Tammikuu", 54 | "february": "Helmikuu", 55 | "march": "Maaliskuu", 56 | "april": "Huhtikuu", 57 | "may": "Toukokuu", 58 | "june": "Kesäkuu", 59 | "july": "Heinäkuu", 60 | "august": "Elokuu", 61 | "september": "Syyskuu", 62 | "october": "Lokakuu", 63 | "november": "Marraskuu", 64 | "december": "Joulukuu" 65 | }, 66 | "daysAbbreviated": { 67 | "monday": "Ma", 68 | "tuesday": "Ti", 69 | "wednesday": "Ke", 70 | "thursday": "To", 71 | "friday": "Pe", 72 | "saturday": "La", 73 | "sunday": "Su" 74 | } 75 | }, 76 | "DiscardConfirmationModal": { 77 | "title": "Hylkää kaikki tallentamattomat muutokset", 78 | "message": "Jos hylkäät muutokset, poistat kaikki edellisen tallennuksen jälkeen tekemäsi muokkaukset.", 79 | "primaryAction": "Hylkää muutokset", 80 | "secondaryAction": "Jatka muokkausta" 81 | }, 82 | "DropZone": { 83 | "overlayTextFile": "Pudota ladattava tiedosto", 84 | "overlayTextImage": "Pudota lähetettävä kuva", 85 | "errorOverlayTextFile": "Tiedostotyyppi ei kelpaa", 86 | "errorOverlayTextImage": "Kuvan tyyppi ei kelpaa", 87 | "FileUpload": { 88 | "actionTitleFile": "Lisää tiedosto", 89 | "actionTitleImage": "Lisää kuva", 90 | "actionHintFile": "tai pudota lähetettävät tiedostot", 91 | "actionHintImage": "tai pudota lähetettävät kuvat", 92 | "label": "Lataa tiedosto" 93 | } 94 | }, 95 | "EmptySearchResult": { 96 | "altText": "Tyhjennä hakutulokset" 97 | }, 98 | "Frame": { 99 | "skipToContent": "Ohita ja siirry sisältöön", 100 | "Navigation": { 101 | "closeMobileNavigationLabel": "Sulje navigointi" 102 | } 103 | }, 104 | "Icon": { 105 | "backdropWarning": "{color} kuvake ei hyväksy taustoja. Kuvakevärit, joissa on tausta, ovat seuraavat: {colorsWithBackDrops}" 106 | }, 107 | "ActionMenu": { 108 | "RollupActions": { 109 | "rollupButton": "Toiminnat" 110 | } 111 | }, 112 | "Filters": { 113 | "moreFilters": "Lisää suodattimia", 114 | "filter": "{resourceName}-suodatin", 115 | "noFiltersApplied": "Suodattimia ei ole käytetty", 116 | "cancel": "Peruuta", 117 | "done": "Valmis", 118 | "clearAllFilters": "Tyhjennä kaikki suodattimet", 119 | "clear": "Tyhjennä", 120 | "clearLabel": "Tyhjennä {filterName}", 121 | "moreFiltersWithCount": "Lisää suodattimia ({count})" 122 | }, 123 | "Modal": { 124 | "iFrameTitle": "tekstin merkintä", 125 | "modalWarning": "Nämä vaaditut ominaisuudet puuttuvat Modalista: {missingProps}" 126 | }, 127 | "Pagination": { 128 | "previous": "Edellinen", 129 | "next": "Seuraava", 130 | "pagination": "Sivunumerointi" 131 | }, 132 | "ProgressBar": { 133 | "negativeWarningMessage": "Edistymistietoihin siirretyt arvot eivät saa olla negatiivisia. Palautetaan {progress} arvoon 0.", 134 | "exceedWarningMessage": "Edistymistietoihin siirretyt arvot eivät saa olla suurempia kuin 100. Asetetaan {progress} arvoon 100." 135 | }, 136 | "ResourceList": { 137 | "sortingLabel": "Lajittele:", 138 | "defaultItemSingular": "tuote", 139 | "defaultItemPlural": "tuotteet", 140 | "showing": "Näytetään {itemsCount} {resource}", 141 | "loading": "Ladataan {resource}", 142 | "selected": "{selectedItemsCount} valittu", 143 | "allItemsSelected": "Kaikki {itemsLength}+ {resourceNamePlural} kaupassasi on valittu.", 144 | "selectAllItems": "Valitse kaikki {itemsLength}+ {resourceNamePlural} kaupassasi", 145 | "emptySearchResultTitle": "{resourceNamePlural} ei löytynyt", 146 | "emptySearchResultDescription": "Yritä vaihtaa suodattimia tai hakuehtoa", 147 | "selectButtonText": "Valitse", 148 | "a11yCheckboxDeselectAllSingle": "Poista valinta {resourceNameSingular}", 149 | "a11yCheckboxSelectAllSingle": "Valitse {resourceNameSingular}", 150 | "a11yCheckboxDeselectAllMultiple": "Poista kaikki valitut {itemsLength} {resourceNamePlural}", 151 | "a11yCheckboxSelectAllMultiple": "Valitse kaikki {itemsLength} {resourceNamePlural}", 152 | "ariaLiveSingular": "{itemsLength} tuote", 153 | "ariaLivePlural": "{itemsLength} tuotteet", 154 | "Item": { 155 | "actionsDropdownLabel": "Kohdetta {accessibilityLabel} koskevat toimenpiteet", 156 | "actionsDropdown": "Toiminnat-alasvetovalikko", 157 | "viewItem": "Näytä tiedot kohteesta {itemName}" 158 | }, 159 | "BulkActions": { 160 | "actionsActivatorLabel": "Toiminnat", 161 | "moreActionsActivatorLabel": "Lisää toimintoja", 162 | "warningMessage": "Paremman käyttäjäkokemuksen tarjoamiseksi. Mainostettavia toimintoja saa olla enintään {maxPromotedActions}." 163 | }, 164 | "FilterCreator": { 165 | "filterButtonLabel": "Suodata", 166 | "selectFilterKeyPlaceholder": "Valitse suodatin…", 167 | "addFilterButtonLabel": "Lisää suodatin", 168 | "showAllWhere": "Näytä kaikki {resourceNamePlural}, joissa:" 169 | }, 170 | "FilterControl": { 171 | "textFieldLabel": "Hae {resourceNamePlural}" 172 | }, 173 | "FilterValueSelector": { 174 | "selectFilterValuePlaceholder": "Valitse suodatin…" 175 | }, 176 | "DateSelector": { 177 | "dateFilterLabel": "Valitse arvo", 178 | "dateValueLabel": "Päivämäärä", 179 | "dateValueError": "Noudata muotoa VVVV-KK-PP", 180 | "dateValuePlaceholder": "VVVV-KK-PP", 181 | "SelectOptions": { 182 | "PastWeek": "viime viikolla", 183 | "PastMonth": "viime kuussa", 184 | "PastQuarter": "viimeisten kolmen kuukauden aikana", 185 | "PastYear": "viime vuonna", 186 | "ComingWeek": "ensi viikolla", 187 | "ComingMonth": "ensi kuussa", 188 | "ComingQuarter": "seuraavien kolmen kuukauden aikana", 189 | "ComingYear": "ensi vuonna", 190 | "OnOrBefore": "päivänä tai sitä ennen", 191 | "OnOrAfter": "päivänä tai sen jälkeen" 192 | }, 193 | "FilterLabelForValue": { 194 | "past_week": "viime viikolla", 195 | "past_month": "viime kuussa", 196 | "past_quarter": "viimeisten kolmen kuukauden aikana", 197 | "past_year": "viime vuonna", 198 | "coming_week": "ensi viikolla", 199 | "coming_month": "ensi kuussa", 200 | "coming_quarter": "seuraavien kolmen kuukauden aikana", 201 | "coming_year": "ensi vuonna", 202 | "on_or_before": "ennen {date}", 203 | "on_or_after": "jälkeen {date}" 204 | } 205 | }, 206 | "showingTotalCount": "Näytetään {itemsCount}/{totalItemsCount} {resource}" 207 | }, 208 | "SkeletonPage": { 209 | "loadingLabel": "Sivu latautuu" 210 | }, 211 | "Spinner": { 212 | "warningMessage": "Väriä {color} ei ole tarkoitus käyttää koon {size} spinnereissä. Suurten spinnerien värit ovat: {colors}" 213 | }, 214 | "Tabs": { 215 | "toggleTabsLabel": "Lisää välilehtiä" 216 | }, 217 | "Tag": { 218 | "ariaLabel": "Poista {children}" 219 | }, 220 | "TextField": { 221 | "characterCount": "{count} merkkiä", 222 | "characterCountWithMaxLength": "{count} / {limit} merkistä käytetty" 223 | }, 224 | "TopBar": { 225 | "toggleMenuLabel": "Vaihda valikkoa", 226 | "SearchField": { 227 | "clearButtonLabel": "Tyhjennä", 228 | "search": "Hae" 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/locales/hi.json: -------------------------------------------------------------------------------- 1 | { 2 | "Polaris": { 3 | "Avatar": { 4 | "label": "अवतार", 5 | "labelWithInitials": "शुरू अक्षर {initials} के साथ अवतार" 6 | }, 7 | "Autocomplete": { 8 | "spinnerAccessibilityLabel": "लोड हो रहा है" 9 | }, 10 | "Badge": { 11 | "PROGRESS_LABELS": { 12 | "incomplete": "अधूरा", 13 | "partiallyComplete": "आंशिक रूप से पूर्ण", 14 | "complete": "पूर्ण" 15 | }, 16 | "STATUS_LABELS": { 17 | "info": "जानकारी", 18 | "success": "सफल", 19 | "warning": "चेतावनी", 20 | "attention": "ध्यान दें", 21 | "new": "नया" 22 | } 23 | }, 24 | "Button": { 25 | "spinnerAccessibilityLabel": "लोड हो रहा है", 26 | "connectedDisclosureAccessibilityLabel": "संबंधित कार्रवाइयाँ" 27 | }, 28 | "Common": { 29 | "checkbox": "चेकबॉक्स", 30 | "undo": "पूर्ववत करें", 31 | "cancel": "रद्द करें", 32 | "newWindowAccessibilityHint": "(नई विंडो खोलता है)", 33 | "clear": "मिटाएं", 34 | "close": "बंद करे", 35 | "submit": "सबमिट करें", 36 | "more": "अधिक" 37 | }, 38 | "ContextualSaveBar": { 39 | "save": "सहेजें", 40 | "discard": "छोड़ें" 41 | }, 42 | "DataTable": { 43 | "sortAccessibilityLabel": "{direction} के अनुसार क्रमबद्ध करें", 44 | "navAccessibilityLabel": "तालिका को एक कॉलम {direction} ओर स्क्रॉल करें", 45 | "totalsRowHeading": "कुल", 46 | "totalRowHeading": "कुल" 47 | }, 48 | "DatePicker": { 49 | "previousMonth": "पिछला महीना दिखाएं, {previousMonthName} {showPreviousYear}", 50 | "nextMonth": "अगला महीना दिखाएं, {nextMonth} {nextYear}", 51 | "today": "आज ", 52 | "months": { 53 | "january": "जनवरी", 54 | "february": "फरवरी", 55 | "march": "मार्च", 56 | "april": "अप्रैल", 57 | "may": "मई", 58 | "june": "जून", 59 | "july": "जुलाई", 60 | "august": "अगस्त", 61 | "september": "सितंबर", 62 | "october": "अक्टूबर", 63 | "november": "नवंबर", 64 | "december": "दिसंबर" 65 | }, 66 | "daysAbbreviated": { 67 | "monday": "मो", 68 | "tuesday": "मंगल", 69 | "wednesday": "बुध", 70 | "thursday": "गुरु", 71 | "friday": "शुक्र", 72 | "saturday": "शनि", 73 | "sunday": "रवि" 74 | } 75 | }, 76 | "DiscardConfirmationModal": { 77 | "title": "सहेजे नहीं गए बदलावों को छोड़ दें", 78 | "message": "यदि आप परिवर्तन छोड़ देते हैं तो आप अपने द्वारा पिछली बार सहेजे गए सभी संपादन हटा देंगे.", 79 | "primaryAction": "परिवर्तनों को छोड़ दें", 80 | "secondaryAction": "संपादन जारी रखें" 81 | }, 82 | "DropZone": { 83 | "overlayTextFile": "अपलोड करने के लिए फाइल ड्रॉप करें", 84 | "overlayTextImage": "अपलोड करने के लिए इमेज ड्रॉप करें", 85 | "errorOverlayTextFile": "फाइल का प्रकार मान्य नहीं है", 86 | "errorOverlayTextImage": "इमेज का प्रकार मान्य नहीं है", 87 | "FileUpload": { 88 | "actionTitleFile": "फाइल जोड़ें", 89 | "actionTitleImage": "इमेज जोड़ें", 90 | "actionHintFile": "या अपलोड करने के लिए फ़ाइलें ड्रॉप करें", 91 | "actionHintImage": "या अपलोड करने के लिए इमेज ड्रॉप करें", 92 | "label": "फ़ाइल अपलोड करें" 93 | } 94 | }, 95 | "EmptySearchResult": { 96 | "altText": "खोज परिणाम खाली" 97 | }, 98 | "Frame": { 99 | "skipToContent": "सामग्री को छोड़ें", 100 | "Navigation": { 101 | "closeMobileNavigationLabel": "नेविगेशन बंद करें" 102 | } 103 | }, 104 | "Icon": { 105 | "backdropWarning": "{color} आइकन बैकड्रॉप स्वीकार नहीं करता है. आइकन के वे रंग जिनमें बैकड्रॉप होते हैं: {colorsWithBackDrops}" 106 | }, 107 | "ActionMenu": { 108 | "RollupActions": { 109 | "rollupButton": "कार्रवाई" 110 | } 111 | }, 112 | "Filters": { 113 | "moreFilters": "अधिक फ़िल्टर", 114 | "filter": "फ़िल्टर {resourceName}", 115 | "noFiltersApplied": "कोई फ़िल्टर लागू नहीं किए गए", 116 | "cancel": "रद्द करें", 117 | "done": "पूरा हुआ", 118 | "clearAllFilters": "सभी फ़िल्टर मिटाएं", 119 | "clear": "मिटाएं", 120 | "clearLabel": "{filterName} मिटाएं", 121 | "moreFiltersWithCount": "और फ़िल्टर्स ({count})" 122 | }, 123 | "Modal": { 124 | "iFrameTitle": "बॉडी मार्कअप", 125 | "modalWarning": "मॉडल में ये आवश्यक विशेषताएं नहीं हैं: {missingProps}" 126 | }, 127 | "Pagination": { 128 | "previous": "पिछला", 129 | "next": "अगला", 130 | "pagination": "पेज क्रमांक डालना" 131 | }, 132 | "ProgressBar": { 133 | "negativeWarningMessage": "प्रगति विशेषता में भेजे गए मूल्य नकारात्मक नहीं होने चाहिए. {progress} को 0 पर सेट किया जा रहा", 134 | "exceedWarningMessage": "प्रगति विशेषता में भेजे गए मूल्य 100 से अधिक नहीं होना चाहिए. {progress} को 100 पर किया जा रहा है" 135 | }, 136 | "ResourceList": { 137 | "sortingLabel": "द्वारा क्रमबद्ध करें", 138 | "defaultItemSingular": "आइटम", 139 | "defaultItemPlural": "आइटम", 140 | "showing": "{itemsCount} {resource} दिखा रहे हैं", 141 | "loading": "{resource} लोड हो रहा है", 142 | "selected": "{selectedItemsCount} चयनित", 143 | "allItemsSelected": "आपके स्टोर के सभी {itemsLength}+ {resourceNamePlural} चुने गए.", 144 | "selectAllItems": "आपके स्टोर के सभी {itemsLength}+ {resourceNamePlural} चुनें", 145 | "emptySearchResultTitle": "कोई {resourceNamePlural} नहीं मिला", 146 | "emptySearchResultDescription": "फ़िल्टर या खोज शब्द बदलने का प्रयास करें", 147 | "selectButtonText": "चुनें", 148 | "a11yCheckboxDeselectAllSingle": "{resourceNameSingular} का चयन रद्द करें", 149 | "a11yCheckboxSelectAllSingle": "{resourceNameSingular} चुनें", 150 | "a11yCheckboxDeselectAllMultiple": "सभी {itemsLength} {resourceNamePlural} का चयन रद्द करें", 151 | "a11yCheckboxSelectAllMultiple": "सभी {itemsLength} {resourceNamePlural} को चुनें", 152 | "ariaLiveSingular": "{itemsLength} आइटम", 153 | "ariaLivePlural": "{itemsLength} आइटम", 154 | "Item": { 155 | "actionsDropdownLabel": "{accessibilityLabel} के लिए कार्रवाई", 156 | "actionsDropdown": "कार्रवाई ड्रॉपडाउन", 157 | "viewItem": "{itemName} के लिए विवरण देखें" 158 | }, 159 | "BulkActions": { 160 | "actionsActivatorLabel": "कार्रवाई", 161 | "moreActionsActivatorLabel": "अधिक कार्रवाई", 162 | "warningMessage": "बेहतर उपयोगकर्ता अनुभव प्रदान करने के लिए. केवल अधिकतम {maxPromotedActions} प्रचारित कार्रवाइयां होनी चाहिए." 163 | }, 164 | "FilterCreator": { 165 | "filterButtonLabel": "फ़िल्टर", 166 | "selectFilterKeyPlaceholder": "फ़िल्टर चुनें...", 167 | "addFilterButtonLabel": "फ़िल्टर जोड़ें", 168 | "showAllWhere": "सभी {resourceNamePlural} दिखाएँ जहाँ:" 169 | }, 170 | "FilterControl": { 171 | "textFieldLabel": "{resourceNamePlural} खोजें" 172 | }, 173 | "FilterValueSelector": { 174 | "selectFilterValuePlaceholder": "फ़िल्टर चुनें..." 175 | }, 176 | "DateSelector": { 177 | "dateFilterLabel": "मूल्य चुनें", 178 | "dateValueLabel": "दिनांक", 179 | "dateValueError": "DD-MM-YYYY फ़ॉर्मेट से मिलान", 180 | "dateValuePlaceholder": "YYYY-MM-DD", 181 | "SelectOptions": { 182 | "PastWeek": "पिछले हफ़्ते", 183 | "PastMonth": "पिछले महीने", 184 | "PastQuarter": "पिछले 3 महीने", 185 | "PastYear": "पिछले साल", 186 | "ComingWeek": "अगले हफ़्ते", 187 | "ComingMonth": "अगले महीने", 188 | "ComingQuarter": "अगले 3 महीनों", 189 | "ComingYear": "अगले वर्ष", 190 | "OnOrBefore": "इस पर या इसके पहले", 191 | "OnOrAfter": "इस पर या इसके बाद" 192 | }, 193 | "FilterLabelForValue": { 194 | "past_week": "पिछले हफ़्ते", 195 | "past_month": "पिछले महीने", 196 | "past_quarter": "पिछले 3 महीने में", 197 | "past_year": "पिछले साल में", 198 | "coming_week": "अगले हफ़्ते", 199 | "coming_month": "अगले महीने", 200 | "coming_quarter": "अगले 3 महीनों में", 201 | "coming_year": "अगले वर्ष", 202 | "on_or_before": "{date} से पहले", 203 | "on_or_after": "{date} के बाद" 204 | } 205 | }, 206 | "showingTotalCount": "{totalItemsCount} {resource} का {itemsCount} दिखा रहा है" 207 | }, 208 | "SkeletonPage": { 209 | "loadingLabel": "पेज लोड हो रहा है" 210 | }, 211 | "Spinner": { 212 | "warningMessage": "{color} रंग का उपयोग {size} स्पिनर पर करने के लिए नहीं है. बड़े स्पिनर पर उपलब्ध रंग हैं: {colors}" 213 | }, 214 | "Tabs": { 215 | "toggleTabsLabel": "अधिक टैब" 216 | }, 217 | "Tag": { 218 | "ariaLabel": "{children} को निकालें" 219 | }, 220 | "TextField": { 221 | "characterCount": "{count} वर्ण", 222 | "characterCountWithMaxLength": "{limit} वर्णों में से {count} का उपयोग किया गया" 223 | }, 224 | "TopBar": { 225 | "toggleMenuLabel": "मेनू टॉगल करें", 226 | "SearchField": { 227 | "clearButtonLabel": "मिटाएं", 228 | "search": "खोजें" 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "Polaris": { 3 | "Avatar": { 4 | "label": "アバター", 5 | "labelWithInitials": "頭文字が{initials}のアバター" 6 | }, 7 | "Autocomplete": { 8 | "spinnerAccessibilityLabel": "読み込んでいます" 9 | }, 10 | "Badge": { 11 | "PROGRESS_LABELS": { 12 | "incomplete": "未完了", 13 | "partiallyComplete": "一部完了済", 14 | "complete": "完了" 15 | }, 16 | "STATUS_LABELS": { 17 | "info": "情報", 18 | "success": "成功", 19 | "warning": "警告", 20 | "attention": "注意", 21 | "new": "新規" 22 | } 23 | }, 24 | "Button": { 25 | "spinnerAccessibilityLabel": "読み込んでいます", 26 | "connectedDisclosureAccessibilityLabel": "関連したアクション" 27 | }, 28 | "Common": { 29 | "checkbox": "チェックボックス", 30 | "undo": "元に戻す", 31 | "cancel": "キャンセルする", 32 | "newWindowAccessibilityHint": "(新しいウィンドウを開く)", 33 | "clear": "クリア", 34 | "close": "閉じる", 35 | "submit": "送信する", 36 | "more": "さらに" 37 | }, 38 | "ContextualSaveBar": { 39 | "save": "保存する", 40 | "discard": "破棄する" 41 | }, 42 | "DataTable": { 43 | "sortAccessibilityLabel": "で{direction}を並び替える", 44 | "navAccessibilityLabel": "表{direction}を1列スクロールする", 45 | "totalsRowHeading": "合計", 46 | "totalRowHeading": "総" 47 | }, 48 | "DatePicker": { 49 | "previousMonth": "先月{showPreviousYear}{previousMonthName}を表示する", 50 | "nextMonth": "来月{nextYear}{nextMonth}を表示する", 51 | "today": "今日 ", 52 | "months": { 53 | "january": "1月", 54 | "february": "2月", 55 | "march": "3月", 56 | "april": "4月", 57 | "may": "5月", 58 | "june": "6月", 59 | "july": "7月", 60 | "august": "8月", 61 | "september": "9月", 62 | "october": "10月", 63 | "november": "11月", 64 | "december": "12月" 65 | }, 66 | "daysAbbreviated": { 67 | "monday": "月", 68 | "tuesday": "火", 69 | "wednesday": "水", 70 | "thursday": "木", 71 | "friday": "金", 72 | "saturday": "土", 73 | "sunday": "日" 74 | } 75 | }, 76 | "DiscardConfirmationModal": { 77 | "title": "すべての保存されていない変更を破棄する", 78 | "message": "変更を破棄すると、最後に保存した時以降のすべての編集が削除されます。", 79 | "primaryAction": "変更を破棄する", 80 | "secondaryAction": "編集を続ける" 81 | }, 82 | "DropZone": { 83 | "overlayTextFile": "ファイルをドロップしてアップロード", 84 | "overlayTextImage": "画像をドロップしてアップロードする", 85 | "errorOverlayTextFile": "ファイルタイプが有効ではありません", 86 | "errorOverlayTextImage": "画像タイプが有効ではありません", 87 | "FileUpload": { 88 | "actionTitleFile": "ファイルを追加する", 89 | "actionTitleImage": "画像を追加する", 90 | "actionHintFile": "または、ファイルをドロップしてアップロードする", 91 | "actionHintImage": "または、画像をドロップしてアップロードする", 92 | "label": "ファイルをアップロード" 93 | } 94 | }, 95 | "EmptySearchResult": { 96 | "altText": "空の検索結果" 97 | }, 98 | "Frame": { 99 | "skipToContent": "コンテンツにスキップする", 100 | "Navigation": { 101 | "closeMobileNavigationLabel": "ナビゲーションを閉じる" 102 | } 103 | }, 104 | "Icon": { 105 | "backdropWarning": "{color}のアイコンは、背景を受け付けません。背景を持つアイコンの色は次のとおりです: {colorsWithBackDrops}" 106 | }, 107 | "ActionMenu": { 108 | "RollupActions": { 109 | "rollupButton": "アクション" 110 | } 111 | }, 112 | "Filters": { 113 | "moreFilters": "詳細な絞り込み", 114 | "filter": "フィルター{resourceName}", 115 | "noFiltersApplied": "絞り込みが適用されていません", 116 | "cancel": "キャンセルする", 117 | "done": "完了", 118 | "clearAllFilters": "すべての絞り込みをクリアする", 119 | "clear": "クリア", 120 | "clearLabel": "{filterName}をクリアする", 121 | "moreFiltersWithCount": "詳細な絞り込み ({count})" 122 | }, 123 | "Modal": { 124 | "iFrameTitle": "ボディのマークアップ", 125 | "modalWarning": "これらの必要なプロパティがモーダルにありません: {missingProps}" 126 | }, 127 | "Pagination": { 128 | "previous": "前へ", 129 | "next": "次へ", 130 | "pagination": "ページネーション" 131 | }, 132 | "ProgressBar": { 133 | "negativeWarningMessage": "進行中のプロパティに渡される値にマイナスは使用できません。{progress}を0にリセットする。", 134 | "exceedWarningMessage": "進行中のプロパティに渡される値は100を超えることはできません。{progress}を100に設定する。" 135 | }, 136 | "ResourceList": { 137 | "sortingLabel": "並び替え", 138 | "defaultItemSingular": "アイテム", 139 | "defaultItemPlural": "アイテム", 140 | "showing": "{itemsCount}個の{resource}を表示しています", 141 | "loading": "{resource}を読み込んでいます", 142 | "selected": "{selectedItemsCount}個を選択済", 143 | "allItemsSelected": "ストアにあるすべての{itemsLength}{resourceNamePlural}が選択されています。", 144 | "selectAllItems": "ストアにあるすべての{itemsLength}{resourceNamePlural}を選択する", 145 | "emptySearchResultTitle": "{resourceNamePlural}が見つかりませんでした", 146 | "emptySearchResultDescription": "絞り込みや検索語を変更してください", 147 | "selectButtonText": "選択する", 148 | "a11yCheckboxDeselectAllSingle": "{resourceNameSingular}の選択を解除する", 149 | "a11yCheckboxSelectAllSingle": "{resourceNameSingular}を選択する", 150 | "a11yCheckboxDeselectAllMultiple": "すべての{itemsLength}{resourceNamePlural}の選択を解除する", 151 | "a11yCheckboxSelectAllMultiple": "すべての{itemsLength}{resourceNamePlural}を選択する", 152 | "ariaLiveSingular": "{itemsLength}個のアイテム", 153 | "ariaLivePlural": "{itemsLength}個のアイテム", 154 | "Item": { 155 | "actionsDropdownLabel": "{accessibilityLabel}のアクション", 156 | "actionsDropdown": "アクションドロップダウン", 157 | "viewItem": "{itemName}の詳細を表示する" 158 | }, 159 | "BulkActions": { 160 | "actionsActivatorLabel": "アクション", 161 | "moreActionsActivatorLabel": "その他の操作", 162 | "warningMessage": "さらによいユーザーエクスペリエンスのために。宣伝のアクションは最大で{maxPromotedActions}回までです。" 163 | }, 164 | "FilterCreator": { 165 | "filterButtonLabel": "絞り込む", 166 | "selectFilterKeyPlaceholder": "絞り込みを選択する...", 167 | "addFilterButtonLabel": "絞り込みを追加する", 168 | "showAllWhere": "すべての{resourceNamePlural}を表示する:" 169 | }, 170 | "FilterControl": { 171 | "textFieldLabel": "{resourceNamePlural}を検索する" 172 | }, 173 | "FilterValueSelector": { 174 | "selectFilterValuePlaceholder": "絞り込みを選択する..." 175 | }, 176 | "DateSelector": { 177 | "dateFilterLabel": "値を選択する", 178 | "dateValueLabel": "日付", 179 | "dateValueError": "YYYY年MM月DD日フォーマットと一致する", 180 | "dateValuePlaceholder": "YYYY年MM月DD日", 181 | "SelectOptions": { 182 | "PastWeek": "先週", 183 | "PastMonth": "先月", 184 | "PastQuarter": "過去3か月", 185 | "PastYear": "昨年", 186 | "ComingWeek": "来週", 187 | "ComingMonth": "来月", 188 | "ComingQuarter": "今後3か月", 189 | "ComingYear": "来年", 190 | "OnOrBefore": "以前", 191 | "OnOrAfter": "以後" 192 | }, 193 | "FilterLabelForValue": { 194 | "past_week": "先週", 195 | "past_month": "先月", 196 | "past_quarter": "過去3か月", 197 | "past_year": "昨年", 198 | "coming_week": "来週", 199 | "coming_month": "来月", 200 | "coming_quarter": "今後3か月", 201 | "coming_year": "来年", 202 | "on_or_before": "{date}以前", 203 | "on_or_after": "{date}以後" 204 | } 205 | }, 206 | "showingTotalCount": "{totalItemsCount}件の{resource}中、{itemsCount}件を表示中" 207 | }, 208 | "SkeletonPage": { 209 | "loadingLabel": "ページの読み込み中" 210 | }, 211 | "Spinner": { 212 | "warningMessage": "{color}色は{size}のスピナーでは使用されていません。大きなスピナーで利用可能な色は次のとおりです: {colors}" 213 | }, 214 | "Tabs": { 215 | "toggleTabsLabel": "その他のタブ" 216 | }, 217 | "Tag": { 218 | "ariaLabel": "{children}を削除する" 219 | }, 220 | "TextField": { 221 | "characterCount": "{count}文字", 222 | "characterCountWithMaxLength": "{count}/{limit}文字使用" 223 | }, 224 | "TopBar": { 225 | "toggleMenuLabel": "メニューを切り替える", 226 | "SearchField": { 227 | "clearButtonLabel": "クリア", 228 | "search": "検索" 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/locales/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "Polaris": { 3 | "Avatar": { 4 | "label": "아바타", 5 | "labelWithInitials": "아바타(이니셜: {initials})" 6 | }, 7 | "Autocomplete": { 8 | "spinnerAccessibilityLabel": "로드 중" 9 | }, 10 | "Badge": { 11 | "PROGRESS_LABELS": { 12 | "incomplete": "미완료", 13 | "partiallyComplete": "일부 완료", 14 | "complete": "완료" 15 | }, 16 | "STATUS_LABELS": { 17 | "info": "정보", 18 | "success": "성공", 19 | "warning": "경고", 20 | "attention": "주의", 21 | "new": "신규" 22 | } 23 | }, 24 | "Button": { 25 | "spinnerAccessibilityLabel": "로드 중", 26 | "connectedDisclosureAccessibilityLabel": "관련 작업" 27 | }, 28 | "Common": { 29 | "checkbox": "확인란", 30 | "undo": "실행 취소", 31 | "cancel": "취소", 32 | "newWindowAccessibilityHint": "(새 창에서 열기)", 33 | "clear": "지우기", 34 | "close": "닫기", 35 | "submit": "제출", 36 | "more": "자세히" 37 | }, 38 | "ContextualSaveBar": { 39 | "save": "저장", 40 | "discard": "버리기" 41 | }, 42 | "DataTable": { 43 | "sortAccessibilityLabel": "{direction} 정렬 기준", 44 | "navAccessibilityLabel": "표를 {direction} 방향으로 1열 스크롤", 45 | "totalsRowHeading": "총계", 46 | "totalRowHeading": "총계" 47 | }, 48 | "DatePicker": { 49 | "previousMonth": "지난 달, {previousMonthName} {showPreviousYear} 표시", 50 | "nextMonth": "다음 달, {nextMonth} {nextYear} 표시", 51 | "today": "오늘 ", 52 | "months": { 53 | "january": "1월", 54 | "february": "2월", 55 | "march": "3월", 56 | "april": "4월", 57 | "may": "5월", 58 | "june": "6월", 59 | "july": "7월", 60 | "august": "8월", 61 | "september": "9월", 62 | "october": "10월", 63 | "november": "11월", 64 | "december": "12월" 65 | }, 66 | "daysAbbreviated": { 67 | "monday": "월요일", 68 | "tuesday": "화요일", 69 | "wednesday": "수요일", 70 | "thursday": "목요일", 71 | "friday": "금요일", 72 | "saturday": "토요일", 73 | "sunday": "일요일" 74 | } 75 | }, 76 | "DiscardConfirmationModal": { 77 | "title": "저장되지 않은 모든 변경 사항 버리기", 78 | "message": "변경을 취소하면 마지막으로 저장한 이후에 편집한 내용이 삭제됩니다.", 79 | "primaryAction": "변경 사항 버리기", 80 | "secondaryAction": "계속 편집" 81 | }, 82 | "DropZone": { 83 | "overlayTextFile": "업로드할 파일 놓기", 84 | "overlayTextImage": "업로드할 이미지 놓기", 85 | "errorOverlayTextFile": "파일 형식이 유효하지 않습니다.", 86 | "errorOverlayTextImage": "이미지 형식이 유효하지 않습니다.", 87 | "FileUpload": { 88 | "actionTitleFile": "파일 추가", 89 | "actionTitleImage": "이미지 추가", 90 | "actionHintFile": "또는 업로드할 파일 놓기", 91 | "actionHintImage": "또는 업로드할 이미지 놓기", 92 | "label": "파일 업로드" 93 | } 94 | }, 95 | "EmptySearchResult": { 96 | "altText": "빈 검색 결과" 97 | }, 98 | "Frame": { 99 | "skipToContent": "콘텐츠로 건너뛰기", 100 | "Navigation": { 101 | "closeMobileNavigationLabel": "탐색 닫기" 102 | } 103 | }, 104 | "Icon": { 105 | "backdropWarning": "{color} 아이콘에는 투명 무늬를 사용할 수 없습니다. 투명 무늬가 있는 아이콘 색상: {colorsWithBackDrops}" 106 | }, 107 | "ActionMenu": { 108 | "RollupActions": { 109 | "rollupButton": "작업" 110 | } 111 | }, 112 | "Filters": { 113 | "moreFilters": "추가 필터", 114 | "filter": "{resourceName} 필터", 115 | "noFiltersApplied": "필터 적용 없음", 116 | "cancel": "취소", 117 | "done": "완료", 118 | "clearAllFilters": "모든 필터 지우기", 119 | "clear": "지우기", 120 | "clearLabel": "{filterName} 지우기", 121 | "moreFiltersWithCount": "추가 필터({count}개)" 122 | }, 123 | "Modal": { 124 | "iFrameTitle": "본문 표시", 125 | "modalWarning": "모달에서 누락된 필수 속성: {missingProps}" 126 | }, 127 | "Pagination": { 128 | "previous": "이전", 129 | "next": "다음", 130 | "pagination": "페이지 매김" 131 | }, 132 | "ProgressBar": { 133 | "negativeWarningMessage": "progress 속성으로 전달되는 값은 음수일 수 없습니다. {progress}을(를) 0으로 재설정합니다.", 134 | "exceedWarningMessage": "progress 속성으로 전달되는 값은 100을 초과할 수 없습니다. {progress}을(를) 100으로 설정합니다." 135 | }, 136 | "ResourceList": { 137 | "sortingLabel": "정렬 기준", 138 | "defaultItemSingular": "품목", 139 | "defaultItemPlural": "품목", 140 | "showing": "{itemsCount}개의 {resource} 표시 중", 141 | "loading": "{resource} 로드 중", 142 | "selected": "{selectedItemsCount}개 선택됨", 143 | "allItemsSelected": "스토어에서 길이가 {itemsLength}보다 긴 {resourceNamePlural}을(를) 모두 선택했습니다.", 144 | "selectAllItems": "스토어에서 길이가 {itemsLength}보다 긴 모든 {resourceNamePlural} 선택", 145 | "emptySearchResultTitle": "{resourceNamePlural} 없음", 146 | "emptySearchResultDescription": "필터나 검색어를 변경해 보십시오.", 147 | "selectButtonText": "선택", 148 | "a11yCheckboxDeselectAllSingle": "{resourceNameSingular} 선택 취소", 149 | "a11yCheckboxSelectAllSingle": "{resourceNameSingular} 선택", 150 | "a11yCheckboxDeselectAllMultiple": "길이가 {itemsLength}인 모든 {resourceNamePlural} 선택 취소", 151 | "a11yCheckboxSelectAllMultiple": "길이가 {itemsLength}인 모든 {resourceNamePlural} 선택", 152 | "ariaLiveSingular": "{itemsLength}개 품목", 153 | "ariaLivePlural": "{itemsLength}개 품목", 154 | "Item": { 155 | "actionsDropdownLabel": "{accessibilityLabel}에 대한 작업", 156 | "actionsDropdown": "작업 드롭다운", 157 | "viewItem": "{itemName}의 세부 정보 보기" 158 | }, 159 | "BulkActions": { 160 | "actionsActivatorLabel": "작업", 161 | "moreActionsActivatorLabel": "기타 작업", 162 | "warningMessage": "더 나은 사용자 환경을 제공하려면 프로모션 작업의 수가 {maxPromotedActions}개 이하여야 합니다." 163 | }, 164 | "FilterCreator": { 165 | "filterButtonLabel": "필터", 166 | "selectFilterKeyPlaceholder": "필터 선택...", 167 | "addFilterButtonLabel": "필터 추가", 168 | "showAllWhere": "다음에 해당하는 모든 {resourceNamePlural} 표시:" 169 | }, 170 | "FilterControl": { 171 | "textFieldLabel": "{resourceNamePlural} 검색" 172 | }, 173 | "FilterValueSelector": { 174 | "selectFilterValuePlaceholder": "필터 선택..." 175 | }, 176 | "DateSelector": { 177 | "dateFilterLabel": "값 선택", 178 | "dateValueLabel": "일", 179 | "dateValueError": "YYYY-MM-DD 형식 일치", 180 | "dateValuePlaceholder": "YYYY-MM-DD", 181 | "SelectOptions": { 182 | "PastWeek": "지난 주", 183 | "PastMonth": "지난 달", 184 | "PastQuarter": "지난 3개월", 185 | "PastYear": "작년", 186 | "ComingWeek": "다음 주", 187 | "ComingMonth": "다음 달", 188 | "ComingQuarter": "다음 3개월", 189 | "ComingYear": "내년", 190 | "OnOrBefore": "해당 날짜 또는 이전", 191 | "OnOrAfter": "해당 날짜 또는 이후" 192 | }, 193 | "FilterLabelForValue": { 194 | "past_week": "지난 주", 195 | "past_month": "지난 달", 196 | "past_quarter": "지난 3개월", 197 | "past_year": "작년", 198 | "coming_week": "다음 주", 199 | "coming_month": "다음 달", 200 | "coming_quarter": "다음 3개월", 201 | "coming_year": "내년", 202 | "on_or_before": "{date} 전", 203 | "on_or_after": "{date} 후" 204 | } 205 | }, 206 | "showingTotalCount": "{totalItemsCount} {resource}의 {itemsCount} 표시" 207 | }, 208 | "SkeletonPage": { 209 | "loadingLabel": "페이지 로딩" 210 | }, 211 | "Spinner": { 212 | "warningMessage": "{color} 색상은 {size} 스피너에 사용할 수 없습니다. 대형 스피너에 사용할 수 있는 색상: {colors}" 213 | }, 214 | "Tabs": { 215 | "toggleTabsLabel": "탭 더 보기" 216 | }, 217 | "Tag": { 218 | "ariaLabel": "{children} 제거" 219 | }, 220 | "TextField": { 221 | "characterCount": "{count} 자", 222 | "characterCountWithMaxLength": "{count}/{limit}자 입력함" 223 | }, 224 | "TopBar": { 225 | "toggleMenuLabel": "토글 메뉴", 226 | "SearchField": { 227 | "clearButtonLabel": "지우기", 228 | "search": "검색" 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/locales/nb.json: -------------------------------------------------------------------------------- 1 | { 2 | "Polaris": { 3 | "Avatar": { 4 | "label": "Avatar", 5 | "labelWithInitials": "Avatar med initialene {initials}" 6 | }, 7 | "Autocomplete": { 8 | "spinnerAccessibilityLabel": "Laster inn" 9 | }, 10 | "Badge": { 11 | "PROGRESS_LABELS": { 12 | "incomplete": "Ufullstendig", 13 | "partiallyComplete": "Delvis fullført", 14 | "complete": "Ferdig" 15 | }, 16 | "STATUS_LABELS": { 17 | "info": "Info", 18 | "success": "Vellykket", 19 | "warning": "Advarsel", 20 | "attention": "Obs", 21 | "new": "Ny" 22 | } 23 | }, 24 | "Button": { 25 | "spinnerAccessibilityLabel": "Laster inn", 26 | "connectedDisclosureAccessibilityLabel": "Relaterte handlinger" 27 | }, 28 | "Common": { 29 | "checkbox": "avmerkingsboks", 30 | "undo": "Angre", 31 | "cancel": "Avbryt", 32 | "newWindowAccessibilityHint": "(åpner et nytt vindu)", 33 | "clear": "Fjern", 34 | "close": "Lukk", 35 | "submit": "Send", 36 | "more": "Mer" 37 | }, 38 | "ContextualSaveBar": { 39 | "save": "Lagre", 40 | "discard": "Forkast" 41 | }, 42 | "DataTable": { 43 | "sortAccessibilityLabel": "sortere {direction} etter", 44 | "navAccessibilityLabel": "Rull tabell {direction} en kolonne", 45 | "totalsRowHeading": "Totalt", 46 | "totalRowHeading": "Totalt" 47 | }, 48 | "DatePicker": { 49 | "previousMonth": "Vis forrige måned, {previousMonthName} {showPreviousYear}", 50 | "nextMonth": "Vis neste måned, {nextMonth} {nextYear}", 51 | "today": "I dag ", 52 | "months": { 53 | "january": "Januar", 54 | "february": "Februar", 55 | "march": "Mars", 56 | "april": "April", 57 | "may": "Mai", 58 | "june": "Juni", 59 | "july": "Juli", 60 | "august": "August", 61 | "september": "September", 62 | "october": "Oktober", 63 | "november": "November", 64 | "december": "Desember" 65 | }, 66 | "daysAbbreviated": { 67 | "monday": "man", 68 | "tuesday": "tir", 69 | "wednesday": "ons", 70 | "thursday": "tor", 71 | "friday": "fre", 72 | "saturday": "lør", 73 | "sunday": "søn" 74 | } 75 | }, 76 | "DiscardConfirmationModal": { 77 | "title": "Forkast alle ulagrede endringer", 78 | "message": "Hvis du forkaster endringer, sletter du alt du har gjort siden du sist gang du lagret.", 79 | "primaryAction": "Forkast endringer", 80 | "secondaryAction": "Fortsett å redigere" 81 | }, 82 | "DropZone": { 83 | "overlayTextFile": "Slipp fil for å laste opp", 84 | "overlayTextImage": "Slipp bilde for å laste opp", 85 | "errorOverlayTextFile": "Filtypen er ikke gyldig", 86 | "errorOverlayTextImage": "Bildetypen er ikke gyldig", 87 | "FileUpload": { 88 | "actionTitleFile": "Legg til en fil", 89 | "actionTitleImage": "Legg til et bilde", 90 | "actionHintFile": "eller slipp filer for å laste opp", 91 | "actionHintImage": "eller slipp bilder for å laste opp", 92 | "label": "Last opp fil" 93 | } 94 | }, 95 | "EmptySearchResult": { 96 | "altText": "Tøm søkeresultater" 97 | }, 98 | "Frame": { 99 | "skipToContent": "Gå videre til innholdet", 100 | "Navigation": { 101 | "closeMobileNavigationLabel": "Lukk navigering" 102 | } 103 | }, 104 | "Icon": { 105 | "backdropWarning": "{color}-ikonet godtar ikke bakgrunnsstiler. Ikonfargene som har bakgrunnsstiler, er: {colorsWithBackDrops}" 106 | }, 107 | "ActionMenu": { 108 | "RollupActions": { 109 | "rollupButton": "Handlinger" 110 | } 111 | }, 112 | "Filters": { 113 | "moreFilters": "Flere filtre", 114 | "filter": "Filter {resourceName}", 115 | "noFiltersApplied": "Ingen filtre ble brukt", 116 | "cancel": "Avbryt", 117 | "done": "Ferdig", 118 | "clearAllFilters": "Fjern alle filtrene", 119 | "clear": "Fjern", 120 | "clearLabel": "Fjern {filterName}", 121 | "moreFiltersWithCount": "Flere filtre ({count})" 122 | }, 123 | "Modal": { 124 | "iFrameTitle": "brødtekstpåslag", 125 | "modalWarning": "Disse nødvendige egenskapene mangler fra Modal: {missingProps}" 126 | }, 127 | "Pagination": { 128 | "previous": "Forrige", 129 | "next": "Neste", 130 | "pagination": "Sideinndeling" 131 | }, 132 | "ProgressBar": { 133 | "negativeWarningMessage": "Verdier som er overført til fremgangsrekvisitten, bør ikke være negative. Tilbakestiller {progress} til 0.", 134 | "exceedWarningMessage": "Verdier som er overført til fremgangsrekvisitten, bør ikke overstige 100. Setter {progress} til 100." 135 | }, 136 | "ResourceList": { 137 | "sortingLabel": "Sorter etter", 138 | "defaultItemSingular": "gjenstand", 139 | "defaultItemPlural": "gjenstander", 140 | "showing": "Viser {itemsCount} {resource}", 141 | "loading": "Laster inn {resource}", 142 | "selected": "{selectedItemsCount} er valgt", 143 | "allItemsSelected": "Alle {itemsLength}+ {resourceNamePlural} i butikken din er valgt.", 144 | "selectAllItems": "Velg alle {itemsLength}+ {resourceNamePlural} i butikken din", 145 | "emptySearchResultTitle": "Fant ingen {resourceNamePlural}", 146 | "emptySearchResultDescription": "Prøv å endre filtrene eller søkeord", 147 | "selectButtonText": "Velg", 148 | "a11yCheckboxDeselectAllSingle": "Opphev valg av {resourceNameSingular}", 149 | "a11yCheckboxSelectAllSingle": "Velg {resourceNameSingular}", 150 | "a11yCheckboxDeselectAllMultiple": "Opphev alle valg av {itemsLength} {resourceNamePlural}", 151 | "a11yCheckboxSelectAllMultiple": "Velg alle {itemsLength} {resourceNamePlural}", 152 | "ariaLiveSingular": "{itemsLength} gjenstand", 153 | "ariaLivePlural": "{itemsLength} gjenstander", 154 | "Item": { 155 | "actionsDropdownLabel": "Handlinger for {accessibilityLabel}", 156 | "actionsDropdown": "Handlinger-rullegardin", 157 | "viewItem": "Se detaljer for {itemName}" 158 | }, 159 | "BulkActions": { 160 | "actionsActivatorLabel": "Handlinger", 161 | "moreActionsActivatorLabel": "Flere handlinger", 162 | "warningMessage": "For å gi en bedre brukeropplevelse. Det bør ikke være mer enn {maxPromotedActions} promoterte handlinger." 163 | }, 164 | "FilterCreator": { 165 | "filterButtonLabel": "Filter", 166 | "selectFilterKeyPlaceholder": "Velg et filter", 167 | "addFilterButtonLabel": "Legg til filter", 168 | "showAllWhere": "Vis alle {resourceNamePlural} der:" 169 | }, 170 | "FilterControl": { 171 | "textFieldLabel": "Søk {resourceNamePlural}" 172 | }, 173 | "FilterValueSelector": { 174 | "selectFilterValuePlaceholder": "Velg et filter" 175 | }, 176 | "DateSelector": { 177 | "dateFilterLabel": "Velg en verdi", 178 | "dateValueLabel": "Dato", 179 | "dateValueError": "Match formatet ÅÅÅÅ-MM-DD", 180 | "dateValuePlaceholder": "ÅÅÅÅ-MM-DD", 181 | "SelectOptions": { 182 | "PastWeek": "i løpet av den siste uken", 183 | "PastMonth": "i løpet av den siste måneden", 184 | "PastQuarter": "i løpet av de siste 3 månedene", 185 | "PastYear": "i løpet av det siste året", 186 | "ComingWeek": "neste uke", 187 | "ComingMonth": "neste måned", 188 | "ComingQuarter": "i løpet av de neste 3 månedene", 189 | "ComingYear": "i løpet av det neste året", 190 | "OnOrBefore": "på eller før", 191 | "OnOrAfter": "på eller etter" 192 | }, 193 | "FilterLabelForValue": { 194 | "past_week": "i løpet av den siste uken", 195 | "past_month": "i løpet av den siste måneden", 196 | "past_quarter": "i løpet av de siste 3 månedene", 197 | "past_year": "i løpet av det siste året", 198 | "coming_week": "neste uke", 199 | "coming_month": "neste måned", 200 | "coming_quarter": "i løpet av de neste 3 månedene", 201 | "coming_year": "i løpet av det neste året", 202 | "on_or_before": "før {date}", 203 | "on_or_after": "etter {date}" 204 | } 205 | }, 206 | "showingTotalCount": "Viser {itemsCount} av {totalItemsCount} {resource}" 207 | }, 208 | "SkeletonPage": { 209 | "loadingLabel": "Siden laster" 210 | }, 211 | "Spinner": { 212 | "warningMessage": "Fargen {color} er ikke ment å bli brukt på {size} spinnere. Fargene som er tilgjengelige på store spinnere, er: {colors}" 213 | }, 214 | "Tabs": { 215 | "toggleTabsLabel": "Flere faner" 216 | }, 217 | "Tag": { 218 | "ariaLabel": "Fjern {children}" 219 | }, 220 | "TextField": { 221 | "characterCount": "{count} tegn", 222 | "characterCountWithMaxLength": "{count} av {limit} tegn brukt" 223 | }, 224 | "TopBar": { 225 | "toggleMenuLabel": "Aktiver/deaktiver meny", 226 | "SearchField": { 227 | "clearButtonLabel": "Fjern", 228 | "search": "Søk" 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/locales/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Polaris": { 3 | "Avatar": { 4 | "label": "Avatar", 5 | "labelWithInitials": "Avatar z inicjałami {initials}" 6 | }, 7 | "Autocomplete": { 8 | "spinnerAccessibilityLabel": "Ładowanie" 9 | }, 10 | "Badge": { 11 | "PROGRESS_LABELS": { 12 | "incomplete": "Niekompletne", 13 | "partiallyComplete": "Częściowo kompletne", 14 | "complete": "Zakończ" 15 | }, 16 | "STATUS_LABELS": { 17 | "info": "Informacja", 18 | "success": "Udało się", 19 | "warning": "Ostrzeżenie", 20 | "attention": "Uwaga", 21 | "new": "Nowe" 22 | } 23 | }, 24 | "Button": { 25 | "spinnerAccessibilityLabel": "Ładowanie", 26 | "connectedDisclosureAccessibilityLabel": "Powiązane czynności" 27 | }, 28 | "Common": { 29 | "checkbox": "pole wyboru", 30 | "undo": "Cofnij", 31 | "cancel": "Anuluj", 32 | "newWindowAccessibilityHint": "(otwiera nowe okno)", 33 | "clear": "Wyczyść", 34 | "close": "Zamknij", 35 | "submit": "Zatwierdź", 36 | "more": "Więcej" 37 | }, 38 | "ContextualSaveBar": { 39 | "save": "Zapisz", 40 | "discard": "Odrzuć" 41 | }, 42 | "DataTable": { 43 | "sortAccessibilityLabel": "sortuj {direction} według", 44 | "navAccessibilityLabel": "Przewiń tabelę {direction} - jedna kolumna", 45 | "totalsRowHeading": "Sumy", 46 | "totalRowHeading": "Suma" 47 | }, 48 | "DatePicker": { 49 | "previousMonth": "Pokaż poprzedni miesiąc, {previousMonthName} {showPreviousYear}", 50 | "nextMonth": "Pokaż następny miesiąc, {nextMonth} {nextYear}", 51 | "today": "Dzisiaj ", 52 | "months": { 53 | "january": "Styczeń", 54 | "february": "Luty", 55 | "march": "Marzec", 56 | "april": "Kwiecień", 57 | "may": "Maj", 58 | "june": "Czerwiec", 59 | "july": "Lipiec", 60 | "august": "Sierpień", 61 | "september": "Wrzesień", 62 | "october": "Październik", 63 | "november": "Listopad", 64 | "december": "Grudzień" 65 | }, 66 | "daysAbbreviated": { 67 | "monday": "Pn", 68 | "tuesday": "Wt", 69 | "wednesday": "Śr", 70 | "thursday": "Cz", 71 | "friday": "Pt", 72 | "saturday": "Sb", 73 | "sunday": "Nd" 74 | } 75 | }, 76 | "DiscardConfirmationModal": { 77 | "title": "Odrzuć wszystkie niezapisane zmiany", 78 | "message": "Jeśli odrzucisz zmiany, usuniesz wszelkie zmiany wprowadzone od czasu ostatniego zapisania.", 79 | "primaryAction": "Odrzuć zmiany", 80 | "secondaryAction": "Kontynuuj edycję" 81 | }, 82 | "DropZone": { 83 | "overlayTextFile": "Upuść plik, aby załadować", 84 | "overlayTextImage": "Upuść obraz , aby załadować", 85 | "errorOverlayTextFile": "Nieprawidłowy typ pliku", 86 | "errorOverlayTextImage": "Nieprawidłowy typ obrazu", 87 | "FileUpload": { 88 | "actionTitleFile": "Dodaj plik", 89 | "actionTitleImage": "Dodaj obraz", 90 | "actionHintFile": "lub upuść pliki, aby załadować", 91 | "actionHintImage": "lub upuść obrazy, aby załadować", 92 | "label": "Przekaż plik" 93 | } 94 | }, 95 | "EmptySearchResult": { 96 | "altText": "Wyczyść wyniki wyszukiwania" 97 | }, 98 | "Frame": { 99 | "skipToContent": "Przejdź do treści", 100 | "Navigation": { 101 | "closeMobileNavigationLabel": "Zamknij nawigację" 102 | } 103 | }, 104 | "Icon": { 105 | "backdropWarning": "Ikona {color} nie akceptuje teł. Kolory ikon, które mają tło, są: {colorsWithBackDrops}" 106 | }, 107 | "ActionMenu": { 108 | "RollupActions": { 109 | "rollupButton": "Czynności" 110 | } 111 | }, 112 | "Filters": { 113 | "moreFilters": "Więcej filtrów", 114 | "filter": "Filtr {resourceName}", 115 | "noFiltersApplied": "Nie zastosowano filtrów", 116 | "cancel": "Anuluj", 117 | "done": "Gotowe", 118 | "clearAllFilters": "Wyczyść wszystkie filtry", 119 | "clear": "Wyczyść", 120 | "clearLabel": "Wyczyść {filterName}", 121 | "moreFiltersWithCount": "Więcej filtrów ({count})" 122 | }, 123 | "Modal": { 124 | "iFrameTitle": "treść - marża", 125 | "modalWarning": "W oknie modalnym brakuje tych wymaganych właściwości: {missingProps}" 126 | }, 127 | "Pagination": { 128 | "previous": "Poprzedni", 129 | "next": "Kolejny", 130 | "pagination": "Paginacja" 131 | }, 132 | "ProgressBar": { 133 | "negativeWarningMessage": "Wartości przekazywane do właściwości postępu nie powinny być negatywne. Resetowanie {progress} do 0.", 134 | "exceedWarningMessage": "Wartości przekazywane do właściwości postępu nie powinny przekroczyć 100. Ustawianie {progress} na 100." 135 | }, 136 | "ResourceList": { 137 | "sortingLabel": "Sortuj wg", 138 | "defaultItemSingular": "pozycji", 139 | "defaultItemPlural": "pozycji", 140 | "showing": "Wyświetlanie {itemsCount} {resource}", 141 | "loading": "Ładowanie {resource}", 142 | "selected": "Wybrano {selectedItemsCount}", 143 | "allItemsSelected": "Wszystkie {itemsLength}+ {resourceNamePlural} w Twoim sklepie są zaznaczone.", 144 | "selectAllItems": "Zaznacz wszystkie {itemsLength}+ {resourceNamePlural} w Twoim sklepie", 145 | "emptySearchResultTitle": "Nie znaleziono {resourceNamePlural}", 146 | "emptySearchResultDescription": "Spróbuj zmienić filtry lub szukany termin", 147 | "selectButtonText": "Wybierz", 148 | "a11yCheckboxDeselectAllSingle": "Usuń wybór {resourceNameSingular}", 149 | "a11yCheckboxSelectAllSingle": "Wybierz {resourceNameSingular}", 150 | "a11yCheckboxDeselectAllMultiple": "Usuń wybór wszystkich {itemsLength} {resourceNamePlural}", 151 | "a11yCheckboxSelectAllMultiple": "Wybierz wszystkie {itemsLength} {resourceNamePlural}", 152 | "ariaLiveSingular": "{itemsLength} pozycja", 153 | "ariaLivePlural": "{itemsLength} pozycje(-i)", 154 | "Item": { 155 | "actionsDropdownLabel": "Czynności dla {accessibilityLabel}", 156 | "actionsDropdown": "Lista rozwijana czynności", 157 | "viewItem": "Wyświetl szczegóły dla {itemName}" 158 | }, 159 | "BulkActions": { 160 | "actionsActivatorLabel": "Czynności", 161 | "moreActionsActivatorLabel": "Więcej czynności", 162 | "warningMessage": "Aby zapewnić większy komfort użytkowania, maksymalna liczba promowanych czynności powinna wynosić tylko {maxPromotedActions}." 163 | }, 164 | "FilterCreator": { 165 | "filterButtonLabel": "Filtr", 166 | "selectFilterKeyPlaceholder": "Wybierz filtr…", 167 | "addFilterButtonLabel": "Dodaj filtr", 168 | "showAllWhere": "Pokaż wszystkie {resourceNamePlural}, gdzie:" 169 | }, 170 | "FilterControl": { 171 | "textFieldLabel": "Szukaj {resourceNamePlural}" 172 | }, 173 | "FilterValueSelector": { 174 | "selectFilterValuePlaceholder": "Wybierz filtr…" 175 | }, 176 | "DateSelector": { 177 | "dateFilterLabel": "Wybierz wartość", 178 | "dateValueLabel": "Data", 179 | "dateValueError": "Dopasuj format RRRR-MM-DD", 180 | "dateValuePlaceholder": "RRRR-MM-DD", 181 | "SelectOptions": { 182 | "PastWeek": "w ostatnim tygodniu", 183 | "PastMonth": "w ostatnim miesiącu", 184 | "PastQuarter": "w ostatnich 3 miesiącach", 185 | "PastYear": "w ostatnim roku", 186 | "ComingWeek": "w następnym tygodniu", 187 | "ComingMonth": "w następnym miesiącu", 188 | "ComingQuarter": "w następnych 3 miesiącach", 189 | "ComingYear": "w następnym roku", 190 | "OnOrBefore": "w tym dniu lub wcześniej", 191 | "OnOrAfter": "w tym dniu lub po" 192 | }, 193 | "FilterLabelForValue": { 194 | "past_week": "w ostatnim tygodniu", 195 | "past_month": "w ostatnim miesiącu", 196 | "past_quarter": "w ostatnich 3 miesiącach", 197 | "past_year": "w ostatnim roku", 198 | "coming_week": "w następnym tygodniu", 199 | "coming_month": "w następnym miesiącu", 200 | "coming_quarter": "w następnych 3 miesiącach", 201 | "coming_year": "w następnym roku", 202 | "on_or_before": "przed {date}", 203 | "on_or_after": "po {date}" 204 | } 205 | }, 206 | "showingTotalCount": "Wyświetlanie {itemsCount} z {totalItemsCount} {resource}" 207 | }, 208 | "SkeletonPage": { 209 | "loadingLabel": "Ładowanie strony" 210 | }, 211 | "Spinner": { 212 | "warningMessage": "Kolor {color} nie jest przeznaczony do używania na pokrętłach {size}. Kolory dostępne na dużych pokrętłach to: {colors}" 213 | }, 214 | "Tabs": { 215 | "toggleTabsLabel": "Więcej kart" 216 | }, 217 | "Tag": { 218 | "ariaLabel": "Usuń {children}" 219 | }, 220 | "TextField": { 221 | "characterCount": "{count} znaki(-ów)", 222 | "characterCountWithMaxLength": "Wykorzystano {count} z {limit} znaków" 223 | }, 224 | "TopBar": { 225 | "toggleMenuLabel": "Przełącz menu", 226 | "SearchField": { 227 | "clearButtonLabel": "Wyczyść", 228 | "search": "Szukaj" 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/locales/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "Polaris": { 3 | "Avatar": { 4 | "label": "Avatar", 5 | "labelWithInitials": "Avatar med initialer {initials}" 6 | }, 7 | "Autocomplete": { 8 | "spinnerAccessibilityLabel": "Laddar" 9 | }, 10 | "Badge": { 11 | "PROGRESS_LABELS": { 12 | "incomplete": "Ofullständig", 13 | "partiallyComplete": "Delvis slutförd", 14 | "complete": "Slutförd" 15 | }, 16 | "STATUS_LABELS": { 17 | "info": "Info", 18 | "success": "Genomfört", 19 | "warning": "Varning", 20 | "attention": "Observera", 21 | "new": "Ny" 22 | } 23 | }, 24 | "Button": { 25 | "spinnerAccessibilityLabel": "Laddar", 26 | "connectedDisclosureAccessibilityLabel": "Relaterade åtgärder" 27 | }, 28 | "Common": { 29 | "checkbox": "kryssruta", 30 | "undo": "Ångra", 31 | "cancel": "Avbryt", 32 | "newWindowAccessibilityHint": "(öppnar ett nytt fönster)", 33 | "clear": "Rensa", 34 | "close": "Stäng", 35 | "submit": "Skicka", 36 | "more": "Mer" 37 | }, 38 | "ContextualSaveBar": { 39 | "save": "Spara", 40 | "discard": "Radera" 41 | }, 42 | "DataTable": { 43 | "sortAccessibilityLabel": "sortera {direction} efter", 44 | "navAccessibilityLabel": "Skrolla tabellen {direction} en kolumn", 45 | "totalsRowHeading": "Totalt", 46 | "totalRowHeading": "Totalt" 47 | }, 48 | "DatePicker": { 49 | "previousMonth": "Visa föregående månad {previousMonthName} {showPreviousYear}", 50 | "nextMonth": "Visa nästa månad {nextMonth} {nextYear}", 51 | "today": "I dag ", 52 | "months": { 53 | "january": "januari", 54 | "february": "februari", 55 | "march": "mars", 56 | "april": "april", 57 | "may": "maj", 58 | "june": "juni", 59 | "july": "juli", 60 | "august": "augusti", 61 | "september": "september", 62 | "october": "oktober", 63 | "november": "november", 64 | "december": "december" 65 | }, 66 | "daysAbbreviated": { 67 | "monday": "mån", 68 | "tuesday": "tis", 69 | "wednesday": "ons", 70 | "thursday": "tor", 71 | "friday": "fre", 72 | "saturday": "lör", 73 | "sunday": "sön" 74 | } 75 | }, 76 | "DiscardConfirmationModal": { 77 | "title": "Ignorera alla ändringar som inte sparats", 78 | "message": "Om du ignorerar ändringar så raderar du alla ändringar som du har gjort efter det att du sparade senast.", 79 | "primaryAction": "Ignorera ändringar", 80 | "secondaryAction": "Fortsätt redigera" 81 | }, 82 | "DropZone": { 83 | "overlayTextFile": "Släpp fil för uppladdning", 84 | "overlayTextImage": "Släpp bild för uppladdning", 85 | "errorOverlayTextFile": "Filtypen är inte giltig", 86 | "errorOverlayTextImage": "Bildtypen är inte giltig", 87 | "FileUpload": { 88 | "actionTitleFile": "Lägg till fil", 89 | "actionTitleImage": "Lägg till bild", 90 | "actionHintFile": "eller släpp filer för uppladdning", 91 | "actionHintImage": "eller släpp bilder för uppladdning", 92 | "label": "Ladda upp fil" 93 | } 94 | }, 95 | "EmptySearchResult": { 96 | "altText": "Tomma sökresultat" 97 | }, 98 | "Frame": { 99 | "skipToContent": "Gå vidare till innehåll", 100 | "Navigation": { 101 | "closeMobileNavigationLabel": "Stäng navigering" 102 | } 103 | }, 104 | "Icon": { 105 | "backdropWarning": "Ikonen {color} accepterar inte bakgrunder. Ikonfärgerna som har bakgrunder är: {colorsWithBackDrops}" 106 | }, 107 | "ActionMenu": { 108 | "RollupActions": { 109 | "rollupButton": "Åtgärder" 110 | } 111 | }, 112 | "Filters": { 113 | "moreFilters": "Fler filter", 114 | "filter": "Filtrera {resourceName}", 115 | "noFiltersApplied": "Inga filter tillämpas", 116 | "cancel": "Avbryt", 117 | "done": "Klar", 118 | "clearAllFilters": "Rensa filter", 119 | "clear": "Rensa", 120 | "clearLabel": "Rensa {filterName}", 121 | "moreFiltersWithCount": "Fler filter ({count})" 122 | }, 123 | "Modal": { 124 | "iFrameTitle": "markering av brödtext", 125 | "modalWarning": "Dessa nödvändiga egenskaper saknas från Modal: {missingProps}" 126 | }, 127 | "Pagination": { 128 | "previous": "Föregående", 129 | "next": "Nästa", 130 | "pagination": "Paginering" 131 | }, 132 | "ProgressBar": { 133 | "negativeWarningMessage": "Värden som skickas till \"progress prop\" bör inte vara negativa. Återställer {progress} till 0.", 134 | "exceedWarningMessage": "Värden som skickas till \"progress prop\" bör inte överstiga 100. Ställ in {progress} till 100." 135 | }, 136 | "ResourceList": { 137 | "sortingLabel": "Sortera efter", 138 | "defaultItemSingular": "artikel", 139 | "defaultItemPlural": "artiklar", 140 | "showing": "Visar {itemsCount} {resource}", 141 | "loading": "Laddar {resource}", 142 | "selected": "{selectedItemsCount} har valts", 143 | "allItemsSelected": "Alla {itemsLength}+ {resourceNamePlural} i din butik har valts.", 144 | "selectAllItems": "Välj alla {itemsLength}+ {resourceNamePlural} i din butik", 145 | "emptySearchResultTitle": "Inga {resourceNamePlural} hittades", 146 | "emptySearchResultDescription": "Prova att byta filter eller sökord", 147 | "selectButtonText": "Välj", 148 | "a11yCheckboxDeselectAllSingle": "Avmarkera {resourceNameSingular}", 149 | "a11yCheckboxSelectAllSingle": "Markera {resourceNameSingular}", 150 | "a11yCheckboxDeselectAllMultiple": "Avmarkera alla {itemsLength} {resourceNamePlural}", 151 | "a11yCheckboxSelectAllMultiple": "Markera alla {itemsLength} {resourceNamePlural}", 152 | "ariaLiveSingular": "{itemsLength} artikel", 153 | "ariaLivePlural": "{itemsLength} artiklar", 154 | "Item": { 155 | "actionsDropdownLabel": "Åtgärder för {accessibilityLabel}", 156 | "actionsDropdown": "Åtgärdsmeny", 157 | "viewItem": "Visa detaljer för {itemName}" 158 | }, 159 | "BulkActions": { 160 | "actionsActivatorLabel": "Åtgärder", 161 | "moreActionsActivatorLabel": "Fler åtgärder", 162 | "warningMessage": "För att tillhandahålla en bättre användarupplevelse bör det vara maximalt {maxPromotedActions} föredragna åtgärder." 163 | }, 164 | "FilterCreator": { 165 | "filterButtonLabel": "Filter", 166 | "selectFilterKeyPlaceholder": "Välj ett filter ...", 167 | "addFilterButtonLabel": "Lägg till filter", 168 | "showAllWhere": "Visa alla {resourceNamePlural} där:" 169 | }, 170 | "FilterControl": { 171 | "textFieldLabel": "Sök {resourceNamePlural}" 172 | }, 173 | "FilterValueSelector": { 174 | "selectFilterValuePlaceholder": "Välj ett filter ..." 175 | }, 176 | "DateSelector": { 177 | "dateFilterLabel": "Välj ett värde", 178 | "dateValueLabel": "Datum", 179 | "dateValueError": "Matcha formatet ÅÅÅÅ-MM-DD", 180 | "dateValuePlaceholder": "ÅÅÅÅ-MM-DD", 181 | "SelectOptions": { 182 | "PastWeek": "under den senaste veckan", 183 | "PastMonth": "under den senaste månaden", 184 | "PastQuarter": "under de senaste 3 månaderna", 185 | "PastYear": "under det senaste året", 186 | "ComingWeek": "nästa vecka", 187 | "ComingMonth": "nästa månad", 188 | "ComingQuarter": "under de närmaste 3 månaderna", 189 | "ComingYear": "under det närmaste året", 190 | "OnOrBefore": "på eller innan", 191 | "OnOrAfter": "på eller efter" 192 | }, 193 | "FilterLabelForValue": { 194 | "past_week": "under den senaste veckan", 195 | "past_month": "under den senaste månaden", 196 | "past_quarter": "under de senaste 3 månaderna", 197 | "past_year": "under det senaste året", 198 | "coming_week": "nästa vecka", 199 | "coming_month": "nästa månad", 200 | "coming_quarter": "under de kommande 3 månaderna", 201 | "coming_year": "under det kommande året", 202 | "on_or_before": "före {date}", 203 | "on_or_after": "efter {date}" 204 | } 205 | }, 206 | "showingTotalCount": "Visar {itemsCount} av {totalItemsCount} {resource}" 207 | }, 208 | "SkeletonPage": { 209 | "loadingLabel": "Sidan laddar" 210 | }, 211 | "Spinner": { 212 | "warningMessage": "Färgen {color} är inte tänkt att användas för {size} spinnare. De färger som finns för stora spinnare är: {colors}" 213 | }, 214 | "Tabs": { 215 | "toggleTabsLabel": "Fler flikar" 216 | }, 217 | "Tag": { 218 | "ariaLabel": "Ta bort {children}" 219 | }, 220 | "TextField": { 221 | "characterCount": "{count} tecken", 222 | "characterCountWithMaxLength": "{count} av {limit} tecken har använts" 223 | }, 224 | "TopBar": { 225 | "toggleMenuLabel": "Växla menyn", 226 | "SearchField": { 227 | "clearButtonLabel": "Rensa", 228 | "search": "Sök" 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/locales/th.json: -------------------------------------------------------------------------------- 1 | { 2 | "Polaris": { 3 | "Avatar": { 4 | "label": "ตัวแทน", 5 | "labelWithInitials": "ตัวแทนพร้อมอักษรย่อ {initials}" 6 | }, 7 | "Autocomplete": { 8 | "spinnerAccessibilityLabel": "กำลังโหลด" 9 | }, 10 | "Badge": { 11 | "PROGRESS_LABELS": { 12 | "incomplete": "ไม่สมบูรณ์", 13 | "partiallyComplete": "สมบูรณ์บางส่วน", 14 | "complete": "สมบูรณ์" 15 | }, 16 | "STATUS_LABELS": { 17 | "info": "ข้อมูล", 18 | "success": "สำเร็จ", 19 | "warning": "คำเตือน", 20 | "attention": "ระวัง", 21 | "new": "ใหม่" 22 | } 23 | }, 24 | "Button": { 25 | "spinnerAccessibilityLabel": "กำลังโหลด", 26 | "connectedDisclosureAccessibilityLabel": "การดำเนินการที่เกี่ยวข้อง" 27 | }, 28 | "Common": { 29 | "checkbox": "ช่องทำเครื่องหมาย", 30 | "undo": "เลิกทำ", 31 | "cancel": "ยกเลิก", 32 | "newWindowAccessibilityHint": "(เปิดหน้าต่างใหม่)", 33 | "clear": "ล้าง", 34 | "close": "ปิด", 35 | "submit": "ส่ง", 36 | "more": "อื่นๆ" 37 | }, 38 | "ContextualSaveBar": { 39 | "save": "บันทึก", 40 | "discard": "ละทิ้ง" 41 | }, 42 | "DataTable": { 43 | "sortAccessibilityLabel": "จัดเรียง {direction} ตาม", 44 | "navAccessibilityLabel": "เลื่อนตาราง {direction} หนึ่งคอลัมน์", 45 | "totalsRowHeading": "รวม", 46 | "totalRowHeading": "ยอดรวม" 47 | }, 48 | "DatePicker": { 49 | "previousMonth": "แสดงเดือนก่อนหน้า {previousMonthName} {showPreviousYear}", 50 | "nextMonth": "แสดงเดือนถัดไป {nextMonth} {nextYear}", 51 | "today": "วันนี้ ", 52 | "months": { 53 | "january": "มกราคม", 54 | "february": "กุมภาพันธ์", 55 | "march": "มีนาคม", 56 | "april": "เมษายน", 57 | "may": "พฤษภาคม", 58 | "june": "มิถุนายน", 59 | "july": "กรกฎาคม", 60 | "august": "สิงหาคม", 61 | "september": "กันยายน", 62 | "october": "ตุลาคม", 63 | "november": "พฤศจิกายน", 64 | "december": "ธันวาคม" 65 | }, 66 | "daysAbbreviated": { 67 | "monday": "จ.", 68 | "tuesday": "อ.", 69 | "wednesday": "พ.", 70 | "thursday": "พฤ.", 71 | "friday": "ศ.", 72 | "saturday": "ส.", 73 | "sunday": "อา." 74 | } 75 | }, 76 | "DiscardConfirmationModal": { 77 | "title": "ละทิ้งการเปลี่ยนแปลงที่ไม่ได้บันทึกทั้งหมด", 78 | "message": "หากคุณละทิ้งการเปลี่ยนแปลง คุณจะลบการแก้ไขทั้งหมดที่คุณดำเนินการหลังการบันทึกล่าสุด", 79 | "primaryAction": "ละทิ้งการเปลี่ยนแปลง", 80 | "secondaryAction": "แก้ไขต่อ" 81 | }, 82 | "DropZone": { 83 | "overlayTextFile": "วางไฟล์เพื่ออัพโหลด", 84 | "overlayTextImage": "วางรูปภาพเพื่ออัพโหลด", 85 | "errorOverlayTextFile": "ประเภทของไฟล์ไม่ถูกต้อง", 86 | "errorOverlayTextImage": "ประเภทของรูปภาพไม่ถูกต้อง", 87 | "FileUpload": { 88 | "actionTitleFile": "เพิ่มไฟล์", 89 | "actionTitleImage": "เพิ่มรูปภาพ", 90 | "actionHintFile": "หรือวางไฟล์เพื่ออัพโหลด", 91 | "actionHintImage": "หรือวางรูปภาพเพื่ออัพโหลด", 92 | "label": "อัปโหลดไฟล์" 93 | } 94 | }, 95 | "EmptySearchResult": { 96 | "altText": "ล้างผลลัพธ์การค้นหา" 97 | }, 98 | "Frame": { 99 | "skipToContent": "ข้ามไปที่เนื้อหา", 100 | "Navigation": { 101 | "closeMobileNavigationLabel": "ปิดการนำทาง" 102 | } 103 | }, 104 | "Icon": { 105 | "backdropWarning": "ไม่สามารถใช้ฉากหลังกับไอคอน {color} ได้ สีไอคอนที่มีฉากหลังคือ: {colorsWithBackDrops}" 106 | }, 107 | "ActionMenu": { 108 | "RollupActions": { 109 | "rollupButton": "การดำเนินการ" 110 | } 111 | }, 112 | "Filters": { 113 | "moreFilters": "ตัวกรองเพิ่มเติม", 114 | "filter": "กรอง {resourceName}", 115 | "noFiltersApplied": "ไม่ได้นำตัวกรองไปใช้", 116 | "cancel": "ยกเลิก", 117 | "done": "เสร็จสิ้น", 118 | "clearAllFilters": "ล้างตัวกรองทั้งหมด", 119 | "clear": "ล้าง", 120 | "clearLabel": "ล้าง {filterName}", 121 | "moreFiltersWithCount": "ตัวกรองเพิ่มเติม ({count})" 122 | }, 123 | "Modal": { 124 | "iFrameTitle": "การแสดงผลเนื้อหา", 125 | "modalWarning": "คุณสมบัติที่กำหนดเหล่านี้ไม่มีอยู่ใน Modal: {missingProps}" 126 | }, 127 | "Pagination": { 128 | "previous": "ก่อนหน้า", 129 | "next": "ถัดไป", 130 | "pagination": "การแบ่งหน้า" 131 | }, 132 | "ProgressBar": { 133 | "negativeWarningMessage": "ค่าที่ส่งไปยังคุณสมบัติความคืบหน้าไม่ควรเป็นลบ กำลังรีเซ็ต {progress} เป็น 0", 134 | "exceedWarningMessage": "ค่าที่ส่งไปยังคุณสมบัติความคืบหน้าไม่ควรเกิน 100 กำลังตั้ง {progress} เป็น 100" 135 | }, 136 | "ResourceList": { 137 | "sortingLabel": "เรียงตาม", 138 | "defaultItemSingular": "รายการ", 139 | "defaultItemPlural": "รายการ", 140 | "showing": "แสดง {itemsCount} {resource}", 141 | "loading": "กำลังโหลด {resource}", 142 | "selected": "{selectedItemsCount} รายการที่เลือก", 143 | "allItemsSelected": "เลือก {itemsLength}+ {resourceNamePlural} ทั้งหมดในร้านค้าของคุณแล้ว", 144 | "selectAllItems": "เลือก {itemsLength}+ {resourceNamePlural} ทั้งหมดในร้านค้าของคุณ", 145 | "emptySearchResultTitle": "ไม่พบ {resourceNamePlural}", 146 | "emptySearchResultDescription": "ลองเปลี่ยนตัวกรองหรือคำค้นหา", 147 | "selectButtonText": "เลือก", 148 | "a11yCheckboxDeselectAllSingle": "ยกเลิกการเลือก {resourceNameSingular}", 149 | "a11yCheckboxSelectAllSingle": "เลือก {resourceNameSingular}", 150 | "a11yCheckboxDeselectAllMultiple": "ยกเลิกการเลือก {itemsLength} {resourceNamePlural} ทั้งหมด", 151 | "a11yCheckboxSelectAllMultiple": "เลือก {itemsLength} {resourceNamePlural} ทั้งหมด", 152 | "ariaLiveSingular": "{itemsLength} รายการ", 153 | "ariaLivePlural": "{itemsLength} รายการ", 154 | "Item": { 155 | "actionsDropdownLabel": "การดำเนินการสำหรับ {accessibilityLabel}", 156 | "actionsDropdown": "เมนูดรอปดาวน์ของการดำเนินการ", 157 | "viewItem": "ดูรายละเอียดของ {itemName}" 158 | }, 159 | "BulkActions": { 160 | "actionsActivatorLabel": "การดำเนินการ", 161 | "moreActionsActivatorLabel": "การดำเนินการเพิ่มเติม", 162 | "warningMessage": "ในการมอบประสบการณ์ใช้งานที่ดียิ่งขึ้นแก่ผู้ใช้ การดำเนินการที่ได้รับการโปรโมทไม่ควรมีมากเกินไปกว่า {maxPromotedActions} รายการ" 163 | }, 164 | "FilterCreator": { 165 | "filterButtonLabel": "ตัวกรอง", 166 | "selectFilterKeyPlaceholder": "เลือกตัวกรอง...", 167 | "addFilterButtonLabel": "เพิ่มตัวกรอง", 168 | "showAllWhere": "แสดง {resourceNamePlural} ทั้งหมดที่:" 169 | }, 170 | "FilterControl": { 171 | "textFieldLabel": "ค้นหา {resourceNamePlural}" 172 | }, 173 | "FilterValueSelector": { 174 | "selectFilterValuePlaceholder": "เลือกตัวกรอง..." 175 | }, 176 | "DateSelector": { 177 | "dateFilterLabel": "เลือกค่า", 178 | "dateValueLabel": "วันที่", 179 | "dateValueError": "จับคู่รูปแบบ ปปปป-ดด-วว", 180 | "dateValuePlaceholder": "ปปปป-ดด-วว", 181 | "SelectOptions": { 182 | "PastWeek": "ในสัปดาห์ที่ผ่านมา", 183 | "PastMonth": "ในเดือนที่ผ่านมา", 184 | "PastQuarter": "ใน 3 เดือนที่ผ่านมา", 185 | "PastYear": "ในปีที่ผ่านมา", 186 | "ComingWeek": "สัปดาห์ถัดไป", 187 | "ComingMonth": "เดือนถัดไป", 188 | "ComingQuarter": "ใน 3 เดือนถัดไป", 189 | "ComingYear": "ในปีถัดไป", 190 | "OnOrBefore": "ในวันดังกล่าวหรือก่อนหน้า", 191 | "OnOrAfter": "ในวันดังกล่าวหรือหลังจาก" 192 | }, 193 | "FilterLabelForValue": { 194 | "past_week": "ในสัปดาห์ที่ผ่านมา", 195 | "past_month": "ในเดือนที่ผ่านมา", 196 | "past_quarter": "ใน 3 เดือนที่ผ่านมา", 197 | "past_year": "ในปีที่ผ่านมา", 198 | "coming_week": "สัปดาห์ถัดไป", 199 | "coming_month": "เดือนถัดไป", 200 | "coming_quarter": "ใน 3 เดือนถัดไป", 201 | "coming_year": "ในปีถัดไป", 202 | "on_or_before": "ก่อน {date}", 203 | "on_or_after": "หลัง {date}" 204 | } 205 | }, 206 | "showingTotalCount": "แสดง {itemsCount} ของ {totalItemsCount} {resource}" 207 | }, 208 | "SkeletonPage": { 209 | "loadingLabel": "กำลังโหลดหน้า" 210 | }, 211 | "Spinner": { 212 | "warningMessage": "สี {color} ไม่ได้มีไว้เพื่อใช้กับสปินเนอร์ขนาด {size} สีที่มีให้บริการสำหรับสปินเนอร์ขนาดใหญ่คือ: {colors}" 213 | }, 214 | "Tabs": { 215 | "toggleTabsLabel": "แท็บเพิ่มเติม" 216 | }, 217 | "Tag": { 218 | "ariaLabel": "ลบ {children} ออก" 219 | }, 220 | "TextField": { 221 | "characterCount": "{count} ตัวอักษร", 222 | "characterCountWithMaxLength": "ใช้ตัวอักษรไปแล้ว {count} จาก {limit}" 223 | }, 224 | "TopBar": { 225 | "toggleMenuLabel": "สลับเมนู", 226 | "SearchField": { 227 | "clearButtonLabel": "ล้าง", 228 | "search": "ค้นหา" 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "Polaris": { 3 | "Avatar": { 4 | "label": "头像", 5 | "labelWithInitials": "头像和姓名缩写 {initials}" 6 | }, 7 | "Autocomplete": { 8 | "spinnerAccessibilityLabel": "正在加载" 9 | }, 10 | "Badge": { 11 | "PROGRESS_LABELS": { 12 | "incomplete": "未完成", 13 | "partiallyComplete": "部分完成", 14 | "complete": "完成" 15 | }, 16 | "STATUS_LABELS": { 17 | "info": "信息", 18 | "success": "成功", 19 | "warning": "警告", 20 | "attention": "注意", 21 | "new": "新建" 22 | } 23 | }, 24 | "Button": { 25 | "spinnerAccessibilityLabel": "正在加载", 26 | "connectedDisclosureAccessibilityLabel": "相关操作" 27 | }, 28 | "Common": { 29 | "checkbox": "复选框", 30 | "undo": "撤销", 31 | "cancel": "取消", 32 | "newWindowAccessibilityHint": "(打开新窗口)", 33 | "clear": "清除", 34 | "close": "关闭", 35 | "submit": "提交", 36 | "more": "更多" 37 | }, 38 | "ContextualSaveBar": { 39 | "save": "保存", 40 | "discard": "取消" 41 | }, 42 | "DataTable": { 43 | "sortAccessibilityLabel": "向 {direction} 排序", 44 | "navAccessibilityLabel": "将表向 {direction} 滚动一列", 45 | "totalsRowHeading": "总计", 46 | "totalRowHeading": "总计" 47 | }, 48 | "DatePicker": { 49 | "previousMonth": "显示上个月,{showPreviousYear} 年 {previousMonthName}", 50 | "nextMonth": "显示下个月,{nextYear} 年 {nextMonth}", 51 | "today": "今天 ", 52 | "months": { 53 | "january": "一月", 54 | "february": "二月", 55 | "march": "三月", 56 | "april": "四月", 57 | "may": "五月", 58 | "june": "六月", 59 | "july": "七月", 60 | "august": "八月", 61 | "september": "九月", 62 | "october": "十月", 63 | "november": "十一月", 64 | "december": "十二月" 65 | }, 66 | "daysAbbreviated": { 67 | "monday": "星期一", 68 | "tuesday": "星期二", 69 | "wednesday": "星期三", 70 | "thursday": "星期四", 71 | "friday": "周五", 72 | "saturday": "星期六", 73 | "sunday": "星期日" 74 | } 75 | }, 76 | "DiscardConfirmationModal": { 77 | "title": "放弃所有未保存的更改", 78 | "message": "如果放弃更改,您将删除自上次保存以来所做的所有编辑。", 79 | "primaryAction": "放弃更改", 80 | "secondaryAction": "继续编辑" 81 | }, 82 | "DropZone": { 83 | "overlayTextFile": "拖放文件以上传", 84 | "overlayTextImage": "拖放图片以上传", 85 | "errorOverlayTextFile": "文件类型无效", 86 | "errorOverlayTextImage": "图片类型无效", 87 | "FileUpload": { 88 | "actionTitleFile": "添加文件", 89 | "actionTitleImage": "添加图片", 90 | "actionHintFile": "或拖放上传", 91 | "actionHintImage": "或拖放上传", 92 | "label": "上传文件" 93 | } 94 | }, 95 | "EmptySearchResult": { 96 | "altText": "清空搜索结果" 97 | }, 98 | "Frame": { 99 | "skipToContent": "跳到内容", 100 | "Navigation": { 101 | "closeMobileNavigationLabel": "关闭网站地图" 102 | } 103 | }, 104 | "Icon": { 105 | "backdropWarning": "{color} 图标不能用于背景。可用于背景的图标颜色:{colorsWithBackDrops}" 106 | }, 107 | "ActionMenu": { 108 | "RollupActions": { 109 | "rollupButton": "编辑" 110 | } 111 | }, 112 | "Filters": { 113 | "moreFilters": "更多筛选器", 114 | "filter": "筛选 {resourceName}", 115 | "noFiltersApplied": "未应用筛选条件", 116 | "cancel": "取消", 117 | "done": "完成", 118 | "clearAllFilters": "清除所有筛选条件", 119 | "clear": "清除", 120 | "clearLabel": "清除 {filterName}", 121 | "moreFiltersWithCount": "更多筛选器({count} 个)" 122 | }, 123 | "Modal": { 124 | "iFrameTitle": "正文标记", 125 | "modalWarning": "模态缺少这些必需的属性:{missingProps}" 126 | }, 127 | "Pagination": { 128 | "previous": "上一页", 129 | "next": "下一页", 130 | "pagination": "分页" 131 | }, 132 | "ProgressBar": { 133 | "negativeWarningMessage": "传递给进度条的值不可为负数。将 {progress} 重置为 0。", 134 | "exceedWarningMessage": "传递给进度条的值不可超过 100。将 {progress} 设置为 100。" 135 | }, 136 | "ResourceList": { 137 | "sortingLabel": "排序方式", 138 | "defaultItemSingular": "件产品", 139 | "defaultItemPlural": "件产品", 140 | "showing": "显示 {itemsCount} 件 {resource}", 141 | "loading": "正在加载 {resource}", 142 | "selected": "已选择 {selectedItemsCount} 件", 143 | "allItemsSelected": "已选择您商店中所有的 {itemsLength}+ 件 {resourceNamePlural}。", 144 | "selectAllItems": "选择商店中所有的 {itemsLength}+ 件 {resourceNamePlural}", 145 | "emptySearchResultTitle": "找不到 {resourceNamePlural}", 146 | "emptySearchResultDescription": "尝试更改筛选条件或搜索词", 147 | "selectButtonText": "选择", 148 | "a11yCheckboxDeselectAllSingle": "取消选择 {resourceNameSingular}", 149 | "a11yCheckboxSelectAllSingle": "选择 {resourceNameSingular}", 150 | "a11yCheckboxDeselectAllMultiple": "取消选择所有 {itemsLength} 件 {resourceNamePlural}", 151 | "a11yCheckboxSelectAllMultiple": "选择所有 {itemsLength} 件 {resourceNamePlural}", 152 | "ariaLiveSingular": "{itemsLength} 件产品", 153 | "ariaLivePlural": "{itemsLength} 件产品", 154 | "Item": { 155 | "actionsDropdownLabel": "{accessibilityLabel}的操作", 156 | "actionsDropdown": "操作下拉菜单", 157 | "viewItem": "查看 {itemName} 详细信息" 158 | }, 159 | "BulkActions": { 160 | "actionsActivatorLabel": "编辑", 161 | "moreActionsActivatorLabel": "其他操作", 162 | "warningMessage": "为了提供更好的用户体验,最多只能有 {maxPromotedActions} 个推广操作。" 163 | }, 164 | "FilterCreator": { 165 | "filterButtonLabel": "筛选", 166 | "selectFilterKeyPlaceholder": "选择筛选条件...", 167 | "addFilterButtonLabel": "添加筛选条件", 168 | "showAllWhere": "显示所有这些 {resourceNamePlural}:" 169 | }, 170 | "FilterControl": { 171 | "textFieldLabel": "搜索 {resourceNamePlural}" 172 | }, 173 | "FilterValueSelector": { 174 | "selectFilterValuePlaceholder": "选择筛选条件..." 175 | }, 176 | "DateSelector": { 177 | "dateFilterLabel": "选择一个值", 178 | "dateValueLabel": "日期", 179 | "dateValueError": "匹配 YYYY-MM-DD 格式", 180 | "dateValuePlaceholder": "YYYY-MM-DD", 181 | "SelectOptions": { 182 | "PastWeek": "过去一周", 183 | "PastMonth": "过去 1 个月", 184 | "PastQuarter": "过去 3 个月", 185 | "PastYear": "过去一年", 186 | "ComingWeek": "下周", 187 | "ComingMonth": "下个月", 188 | "ComingQuarter": "接下来的 3 个月", 189 | "ComingYear": "接下来的一年", 190 | "OnOrBefore": "截至", 191 | "OnOrAfter": "自此以后" 192 | }, 193 | "FilterLabelForValue": { 194 | "past_week": "过去一周", 195 | "past_month": "过去 1 个月", 196 | "past_quarter": "过去 3 个月", 197 | "past_year": "过去一年", 198 | "coming_week": "下周", 199 | "coming_month": "下个月", 200 | "coming_quarter": "接下来的 3 个月", 201 | "coming_year": "接下来的一年", 202 | "on_or_before": "{date} 之前", 203 | "on_or_after": "{date} 之后" 204 | } 205 | }, 206 | "showingTotalCount": "显示 {itemsCount} 项 {resource}(共 {totalItemsCount} 项)" 207 | }, 208 | "SkeletonPage": { 209 | "loadingLabel": "页面加载" 210 | }, 211 | "Spinner": { 212 | "warningMessage": "{size} 加载微调器不支持颜色 {color}。大型加载微调器的可用颜色:{colors}" 213 | }, 214 | "Tabs": { 215 | "toggleTabsLabel": "更多标签" 216 | }, 217 | "Tag": { 218 | "ariaLabel": "删除 {children}" 219 | }, 220 | "TextField": { 221 | "characterCount": "{count} 个字符", 222 | "characterCountWithMaxLength": "使用的字符长度为 {count},限制为 {limit}" 223 | }, 224 | "TopBar": { 225 | "toggleMenuLabel": "切换菜单", 226 | "SearchField": { 227 | "clearButtonLabel": "清除", 228 | "search": "搜索" 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/locales/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "Polaris": { 3 | "Avatar": { 4 | "label": "大頭貼", 5 | "labelWithInitials": "姓名首字母為 {initials} 的大頭貼" 6 | }, 7 | "Autocomplete": { 8 | "spinnerAccessibilityLabel": "載入中" 9 | }, 10 | "Badge": { 11 | "PROGRESS_LABELS": { 12 | "incomplete": "未完成", 13 | "partiallyComplete": "部分完成", 14 | "complete": "完成" 15 | }, 16 | "STATUS_LABELS": { 17 | "info": "資訊", 18 | "success": "成功", 19 | "warning": "警告", 20 | "attention": "注意", 21 | "new": "新增" 22 | } 23 | }, 24 | "Button": { 25 | "spinnerAccessibilityLabel": "載入中", 26 | "connectedDisclosureAccessibilityLabel": "相關動作" 27 | }, 28 | "Common": { 29 | "checkbox": "核取方塊", 30 | "undo": "復原", 31 | "cancel": "取消", 32 | "newWindowAccessibilityHint": "(開啟新視窗)", 33 | "clear": "清除", 34 | "close": "關閉", 35 | "submit": "提交", 36 | "more": "更多" 37 | }, 38 | "ContextualSaveBar": { 39 | "save": "儲存", 40 | "discard": "捨棄" 41 | }, 42 | "DataTable": { 43 | "sortAccessibilityLabel": "向 {direction} 排序,依據", 44 | "navAccessibilityLabel": "向 {direction} 捲動表格一欄", 45 | "totalsRowHeading": "總計", 46 | "totalRowHeading": "總計" 47 | }, 48 | "DatePicker": { 49 | "previousMonth": "顯示上個月,{previousMonthName} {showPreviousYear}", 50 | "nextMonth": "顯示下個月,{nextMonth} {nextYear}", 51 | "today": "今天 ", 52 | "months": { 53 | "january": "一月", 54 | "february": "二月", 55 | "march": "三月", 56 | "april": "四月", 57 | "may": "五月", 58 | "june": "六月", 59 | "july": "七月", 60 | "august": "八月", 61 | "september": "九月", 62 | "october": "十月", 63 | "november": "十一月", 64 | "december": "十二月" 65 | }, 66 | "daysAbbreviated": { 67 | "monday": "星期一", 68 | "tuesday": "星期二", 69 | "wednesday": "星期三", 70 | "thursday": "星期四", 71 | "friday": "星期五", 72 | "saturday": "星期六", 73 | "sunday": "星期日" 74 | } 75 | }, 76 | "DiscardConfirmationModal": { 77 | "title": "捨棄所有尚未儲存的變更內容", 78 | "message": "如果您捨棄變更內容,則會刪除所有從上次儲存以來所編輯過的內容。", 79 | "primaryAction": "捨棄變更內容", 80 | "secondaryAction": "繼續編輯" 81 | }, 82 | "DropZone": { 83 | "overlayTextFile": "拖入檔案以上傳", 84 | "overlayTextImage": "拖入圖片以上傳", 85 | "errorOverlayTextFile": "檔案類型無效", 86 | "errorOverlayTextImage": "圖片類型無效", 87 | "FileUpload": { 88 | "actionTitleFile": "新增檔案", 89 | "actionTitleImage": "新增圖片", 90 | "actionHintFile": "或將檔案拖入即可上傳", 91 | "actionHintImage": "或將圖片拖入即可上傳", 92 | "label": "上傳檔案" 93 | } 94 | }, 95 | "EmptySearchResult": { 96 | "altText": "清空搜尋結果" 97 | }, 98 | "Frame": { 99 | "skipToContent": "跳到內容", 100 | "Navigation": { 101 | "closeMobileNavigationLabel": "關閉導覽" 102 | } 103 | }, 104 | "Icon": { 105 | "backdropWarning": "{color} 圖示不適用背景。有背景的圖示顏色為:{colorsWithBackDrops}" 106 | }, 107 | "ActionMenu": { 108 | "RollupActions": { 109 | "rollupButton": "動作" 110 | } 111 | }, 112 | "Filters": { 113 | "moreFilters": "更多篩選條件", 114 | "filter": "篩選 {resourceName}", 115 | "noFiltersApplied": "未套用篩選條件", 116 | "cancel": "取消", 117 | "done": "完成", 118 | "clearAllFilters": "清除所有篩選條件", 119 | "clear": "清除", 120 | "clearLabel": "清除 {filterName}", 121 | "moreFiltersWithCount": "更多篩選條件 ({count})" 122 | }, 123 | "Modal": { 124 | "iFrameTitle": "本文標記", 125 | "modalWarning": "互動視窗內缺少以下必要屬性:{missingProps}" 126 | }, 127 | "Pagination": { 128 | "previous": "上一頁", 129 | "next": "下一頁", 130 | "pagination": "分頁" 131 | }, 132 | "ProgressBar": { 133 | "negativeWarningMessage": "傳送至進度條屬性的數值不應為負值。將 {progress} 重設為 0。", 134 | "exceedWarningMessage": "傳送至進度條屬性的數值不應超過 100。將 {progress} 設為 100。" 135 | }, 136 | "ResourceList": { 137 | "sortingLabel": "排序方式", 138 | "defaultItemSingular": "商品", 139 | "defaultItemPlural": "商品", 140 | "showing": "顯示 {itemsCount} {resource}", 141 | "loading": "正在載入 {resource}", 142 | "selected": "已選擇 {selectedItemsCount}", 143 | "allItemsSelected": "已選擇您商店中所有 {itemsLength}+ {resourceNamePlural}。", 144 | "selectAllItems": "選擇您商店中所有 {itemsLength}+ {resourceNamePlural}", 145 | "emptySearchResultTitle": "找不到 {resourceNamePlural}", 146 | "emptySearchResultDescription": "嘗試變更篩選條件或搜尋詞彙", 147 | "selectButtonText": "選擇", 148 | "a11yCheckboxDeselectAllSingle": "取消選擇 {resourceNameSingular}", 149 | "a11yCheckboxSelectAllSingle": "選擇 {resourceNameSingular}", 150 | "a11yCheckboxDeselectAllMultiple": "取消選擇全部 {itemsLength} {resourceNamePlural}", 151 | "a11yCheckboxSelectAllMultiple": "選擇全部 {itemsLength} {resourceNamePlural}", 152 | "ariaLiveSingular": "{itemsLength} 件商品", 153 | "ariaLivePlural": "{itemsLength} 件商品", 154 | "Item": { 155 | "actionsDropdownLabel": "{accessibilityLabel} 的動作", 156 | "actionsDropdown": "動作下拉式選單", 157 | "viewItem": "檢視 {itemName} 的詳細資訊" 158 | }, 159 | "BulkActions": { 160 | "actionsActivatorLabel": "動作", 161 | "moreActionsActivatorLabel": "更多動作", 162 | "warningMessage": "為提供更良好的使用者體驗,您一次舉辦的促銷活動應以 {maxPromotedActions} 項為限。" 163 | }, 164 | "FilterCreator": { 165 | "filterButtonLabel": "篩選", 166 | "selectFilterKeyPlaceholder": "選擇篩選條件......", 167 | "addFilterButtonLabel": "新增篩選條件", 168 | "showAllWhere": "顯示符合以下條件的所有 {resourceNamePlural}:" 169 | }, 170 | "FilterControl": { 171 | "textFieldLabel": "搜尋 {resourceNamePlural}" 172 | }, 173 | "FilterValueSelector": { 174 | "selectFilterValuePlaceholder": "選擇篩選條件......" 175 | }, 176 | "DateSelector": { 177 | "dateFilterLabel": "選擇一個值", 178 | "dateValueLabel": "日期", 179 | "dateValueError": "符合「YYYY-MM-DD」格式", 180 | "dateValuePlaceholder": "YYYY-MM-DD", 181 | "SelectOptions": { 182 | "PastWeek": "上週", 183 | "PastMonth": "上個月", 184 | "PastQuarter": "過去 3 個月", 185 | "PastYear": "去年", 186 | "ComingWeek": "下週", 187 | "ComingMonth": "下個月", 188 | "ComingQuarter": "未來 3 個月", 189 | "ComingYear": "明年", 190 | "OnOrBefore": "當天或之前", 191 | "OnOrAfter": "當天或之後" 192 | }, 193 | "FilterLabelForValue": { 194 | "past_week": "上週", 195 | "past_month": "上個月", 196 | "past_quarter": "過去 3 個月", 197 | "past_year": "去年", 198 | "coming_week": "下週", 199 | "coming_month": "下個月", 200 | "coming_quarter": "未來 3 個月", 201 | "coming_year": "明年", 202 | "on_or_before": "{date} 之前", 203 | "on_or_after": "{date} 之後" 204 | } 205 | }, 206 | "showingTotalCount": "顯示第 {itemsCount} 個 {resource},共 {totalItemsCount} 個" 207 | }, 208 | "SkeletonPage": { 209 | "loadingLabel": "頁面載入中" 210 | }, 211 | "Spinner": { 212 | "warningMessage": "{color} 這個顏色不能搭配{size}的轉盤。大型轉盤可以使用的顏色為:{colors}" 213 | }, 214 | "Tabs": { 215 | "toggleTabsLabel": "更多索引標籤" 216 | }, 217 | "Tag": { 218 | "ariaLabel": "移除 {children}" 219 | }, 220 | "TextField": { 221 | "characterCount": "{count} 人物", 222 | "characterCountWithMaxLength": "已使用 {count} 個字元,上限為 {limit} 個字元" 223 | }, 224 | "TopBar": { 225 | "toggleMenuLabel": "切換選單", 226 | "SearchField": { 227 | "clearButtonLabel": "清除", 228 | "search": "搜尋" 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/triangle-of-triangles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leighs-hammer/shopify-app-boilerplate-nextjs-redux-nosql/98948bcee32b9ee418b931baf02dafee4db96795/public/triangle-of-triangles.png -------------------------------------------------------------------------------- /public/triangle.svg: -------------------------------------------------------------------------------- 1 | 8 | 10 | 13 | 17 | 21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | # FEATURETASK 2 | 3 | ## Description 4 | 5 | Add a short meaningful description of what has been done and why 6 | 7 | ## What was done ? 8 | - 9 | 10 | ## Why was this needed 11 | 12 | ## Will dev users need to reinstall their apps? 13 | [yes]/[no] 14 | 15 | ## Is there still any work to complete? 16 | [yes]/[no] 17 | 18 | ## Have you added relevant unit testing? 19 | [yes]/[no] 20 | 21 | ## Have you added a wiki entry? 22 | [yes]/[no] 23 | 24 | if yes link url: 25 | 26 | ## Added any dependencies? 27 | [yes]/[no] -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Shopify App Boilerplate 2 | ![Units](https://github.com/leighs-hammer/shopify-app-boilerplate-nextjs-redux-nosql/workflows/Units/badge.svg) 3 | 4 | uses Nexjs + typescript and designed to be deployed to now.sh, tied in with redux. 5 | For database mongodb atlas is used as an example, but you could pipe in any DB you choose. 6 | The pattern is a hooks first approach to data provisions, surfaced at the page levels serving as HOCs. 7 | Redux is used for persisting state accross & SSR navigation reloads and the usual nested components, it does have its draw backs but at present is the best choice for this handling. 8 | 9 | -- More info: & detailed breakdows -- 10 | 11 | https://github.com/leighs-hammer/shopify-app-boilerplate-nextjs-redux-nosql/wiki 12 | 13 | 14 | ## PreSetup 15 | 1. clone 16 | 2. `npm run install` or yarn of you prefer 17 | 18 | ## Create mongodb atlas db 19 | 1. create a cluster / signup ( https://www.mongodb.com/cloud/atlas ) 20 | 2. create a cluster in a region and on a provider that makes sense to you 21 | 2.1 create a user with roles to access the DB 22 | 3. whitelist open access (0.0.0.0/0) 23 | 4. get your connection string, replace password with the user you created 24 | 5. keep a record of this it will be used in your .env and now secrets 25 | 26 | ## Setup 27 | 1. create an app in your partners.shopify.com dashboard (optionally create a production app as well) 28 | 2. copy the `.env.sample` and create a local `.env` add your development app details and ngrok
29 | 2.1 revise the `/_config/config.ts` specifying the database / root you would like. 30 | 3. open two terminals & start up ngrok `npm run ngrok` in one 31 | 4. copy the https://XXX.ngrok address and load it into the shopify dashboard 32 | 5. whitelist the {NGROK}/dashboard route 33 | 6. in the other temrinal run `npm run dev` 34 | 7. test the app on a dev store, you should see a loading screen and then an empty dashboard. 35 | 36 | ## Multiple environments & deployment via now.sh 37 | I run development and production apps along side each other, this means locally my environment variables are used for my dev app. For a production deployment, you will need environment variables added to your now.sh account. 38 | 39 | `now secrets add APP_URL_NAME value` note that env vars in now are global so you will have a specific name along side them to stop conflicting. you will need these specific to every deployed app. by default it will rename the uppercase to lowercase keys. for example `APP_URL_BOILERPLATE` will become `app_url_boilerplate` load these into the `./now.json` file this will now surface that secret on `process.env.APP_URL` in the app. 40 | 41 | Locally it will use your .env 42 | 43 | 44 | ### example 45 | 46 | ``` 47 | { 48 | "version":2, 49 | "env": { 50 | "APP_URL": "@app_url_boilerplate", 51 | "SHOPIFY_API_KEY": "@shopify_api_key_boilerplate", 52 | "SHOPIFY_APP_SECRET": "@shopify_app_secret_boilerplate", 53 | "SHOPIFY_APP_SCOPES": "@shopify_app_scopes_boilerplate", 54 | 55 | "APP_NAME_KEY": "@app_name_key_boilerplate", 56 | "MONGO_DB_CONNECTION_STRING": "@mongo_db_connection_string_boilerplate" 57 | 58 | }, 59 | "routes": [ 60 | { 61 | "src": "/.*", 62 | "headers": { 63 | "Content-Security-Policy": "frame-ancestors https://*.myshopify.com" 64 | }, 65 | "continue": true 66 | } 67 | ] 68 | } 69 | ``` 70 | 71 | ## Deploying 72 | 73 | Either via commit / mege or manually using `now` || `now --prod` this will deploy to now and your app will be ready to go. 74 | 75 | # USAGE 76 | 77 | The system is pretty much a nextjs app with polaris and some api function `/pages/api/xx.ts` these are always server side and can be used for secure handlings. 78 | 79 | ## Other docs 80 | - Wiki: https://github.com/leighs-hammer/shopify-app-boilerplate-nextjs-redux-nosql/wiki 81 | - Polaris : polaris.shopify.com 82 | - Redux: https://redux.js.org/introduction/getting-started/ 83 | - Immer: https://github.com/immerjs/immer 84 | - mongoDb / Atlas: https://cloud.mongodb.com/ // https://www.mongodb.com/cloud/atlas 85 | - mongoDb Compass: https://www.mongodb.com/products/compass 86 | -------------------------------------------------------------------------------- /tests/utils/arrayContainsArray.unit.test.ts: -------------------------------------------------------------------------------- 1 | import arrayContainsArray from '../../_utils/arrayContainsArray' 2 | 3 | test('should return true when superset contains subset', () => { 4 | const valid = arrayContainsArray([1,2,3,4,5,6], [2,4,6]) 5 | expect(valid).toBe(true) 6 | }) 7 | 8 | test('should return false when superset does not contain a member of the subset', () => { 9 | const valid = arrayContainsArray([1,2,3,4,5,6], [2,4,6,12]) 10 | expect(valid).toBe(false) 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /tests/utils/buildAuthUrl.unit.test.ts: -------------------------------------------------------------------------------- 1 | import buildAuthUrl from '../../_utils/buildAuthUrl' 2 | 3 | test('should expect an auth url to be correct', () => { 4 | const testUrl = buildAuthUrl('test.myshopify.com') 5 | 6 | expect(testUrl).toBe('https://test.myshopify.com/admin/oauth/access_token') 7 | }) 8 | 9 | test('should return false when no shop is passed in', () => { 10 | // @ts-ignore 11 | const failedAuth = buildAuthUrl() 12 | expect(failedAuth).toBe('') 13 | }) -------------------------------------------------------------------------------- /tests/utils/buildGqlEndpoint.unit.test.ts: -------------------------------------------------------------------------------- 1 | import buildGqlEndpoint from '../../_utils/buildGqlEndpoint' 2 | 3 | 4 | test('Should build the correct enpoint without a version', () => { 5 | const endpoint = buildGqlEndpoint('some-shop.myshopify.com') 6 | expect(endpoint).toBe('https://some-shop.myshopify.com/admin/api/2020-01/graphql.json') 7 | }) 8 | 9 | test('Should build the correct enpoint with a version', () => { 10 | const endpointWithVersion = buildGqlEndpoint('some-shop.myshopify.com', '1234561') 11 | expect(endpointWithVersion).toBe('https://some-shop.myshopify.com/admin/api/1234561/graphql.json') 12 | }) 13 | 14 | test('Should return false when no shop is passed', () => { 15 | // @ts-ignore 16 | const failedEndpoint = buildGqlEndpoint() 17 | expect(failedEndpoint).toBe('') 18 | }) 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/utils/buildGqlHeaders.unit.test.ts: -------------------------------------------------------------------------------- 1 | import buildHeaders from '../../_utils/buildGqlHeaders'; 2 | 3 | test('should return correct header token and types', () => { 4 | const headers = buildHeaders('12345') 5 | 6 | expect(headers['X-Shopify-Access-Token']).toBe('12345') 7 | expect(headers['Content-Type']).toBe('application/json') 8 | }) 9 | 10 | test('should return false when no token is passed', () => { 11 | // @ts-ignore 12 | const failedHeaders = buildHeaders() 13 | expect(failedHeaders).toBe(false) 14 | }) -------------------------------------------------------------------------------- /tests/utils/safelyGetNestedKey.unit.test.ts: -------------------------------------------------------------------------------- 1 | import safelyGetNestedText, {getNestedKey} from '../../_utils/safelyGetNestedText' 2 | 3 | const mockDictionary = { 4 | test: { 5 | test: { 6 | test: { 7 | value: 'VALUE', 8 | valueArray: [ 9 | 'string', 10 | 'string2' 11 | ] 12 | } 13 | } 14 | } 15 | } 16 | 17 | 18 | // getNestedKey 19 | test('a nested value should be found with the "getNestedKey" function', () => { 20 | const gotten = getNestedKey(['test','test','test','value'], mockDictionary) 21 | expect(gotten).toBe('VALUE') 22 | }) 23 | 24 | test('a nested value should return false with the "getNestedKey" function', () => { 25 | const gotten = getNestedKey(['test','test','invalid','value'], mockDictionary) 26 | expect(gotten).toBe(false) 27 | }) 28 | 29 | test('a nested array value at an index should be found with the "getNestedKey" function', () => { 30 | const gotten = getNestedKey(['test','test','test','valueArray', '1'], mockDictionary) 31 | expect(gotten).toBe('string2') 32 | }) 33 | 34 | test('a nested array should be found with the "getNestedKey" function', () => { 35 | const gotten = getNestedKey(['test','test','test','valueArray'], mockDictionary) 36 | // @ts-ignore 37 | expect(gotten.length).toBe(2) 38 | expect(gotten[0]).toBe('string') 39 | expect(gotten[1]).toBe('string2') 40 | }) 41 | 42 | // Safely Get or Fallback or fail gracefully 43 | 44 | test('safelyGetNestedText should find nested text using object notation', () => { 45 | const gotten = safelyGetNestedText('test.test.test.value', mockDictionary) 46 | expect(gotten).toBe('VALUE') 47 | }) 48 | 49 | test('safelyGetNestedText should return false text using invalid object notation', () => { 50 | const gotten = safelyGetNestedText('test.invalid.test.value', mockDictionary) 51 | expect(gotten).toBe(false) 52 | }) 53 | 54 | test('safelyGetNestedText should nested array value using object notation', () => { 55 | const gotten = safelyGetNestedText('test.test.test.valueArray.1', mockDictionary) 56 | expect(gotten).toBe('string2') 57 | }) 58 | 59 | test('safelyGetNestedText should nested array using object notation', () => { 60 | const gotten = safelyGetNestedText('test.test.test.valueArray', mockDictionary) 61 | // @ts-ignore 62 | expect(gotten.length).toBe(2) 63 | expect(gotten[0]).toBe('string') 64 | expect(gotten[1]).toBe('string2') 65 | }) -------------------------------------------------------------------------------- /tests/utils/shouldFetchtranslation.unit.test.ts: -------------------------------------------------------------------------------- 1 | import shouldFetchtranslation, {fallbackPreloadedLibrary, fallbackLocale} from '../../_utils/shouldFetchtranslation' 2 | 3 | test('Should return "fallback" when passed the default value', () => { 4 | const shouldNotfetch = shouldFetchtranslation(fallbackLocale) 5 | expect(shouldNotfetch).toBe('fallback') 6 | }) 7 | 8 | test('Should return false when passed an iso location matches', () => { 9 | const shouldNotfetch = shouldFetchtranslation(`${fallbackLocale}-GB`) 10 | expect(shouldNotfetch).toBe('fallback') 11 | }) 12 | 13 | test('Should return a path to a locale when passed a two chr locale', () => { 14 | const shouldNotfetch = shouldFetchtranslation('fr') 15 | expect(shouldNotfetch).toBe('/locales/fr.json') 16 | }) 17 | 18 | test('Should return a path to a locale when passed a two-TWO chr locale', () => { 19 | const shouldNotfetch = shouldFetchtranslation('fr-FR') 20 | expect(shouldNotfetch).toBe('/locales/fr.json') 21 | }) 22 | 23 | test('Should return false when a translation is not enabled.', () => { 24 | const shouldFetch = shouldFetchtranslation('XC-XC') 25 | expect(shouldFetch).toBe('fallback') 26 | }) -------------------------------------------------------------------------------- /tools/migrateStoreData.js: -------------------------------------------------------------------------------- 1 | const migrateStoreDataEntry = () => {} 2 | 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve" 20 | }, 21 | "exclude": [ 22 | "node_modules" 23 | ], 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx", "tests/utils/dataShapeOrders.unit.test.js", "tests/utils/buildGqlEndpoint.unit.test.js" 28 | ] 29 | } 30 | --------------------------------------------------------------------------------