зеркало из https://github.com/mozilla/fxa.git
bug(settings): Hash l10n files for cdn deployment
Because: - We need cache busting file names when deploying new content to the CDN This Commit: - Adds grunt-hash job - Configures grunt has to hash ftl files and create mapping files - Adjusts AppLocalizationProvider to use the mapping file to load l10n files
This commit is contained in:
Родитель
5d1652341a
Коммит
914afe5343
|
@ -150,6 +150,7 @@ packages/fxa-react/test/
|
||||||
|
|
||||||
# fxa-settings
|
# fxa-settings
|
||||||
packages/fxa-settings/fxa-content-server-l10n/
|
packages/fxa-settings/fxa-content-server-l10n/
|
||||||
|
packages/fxa-settings/public/static
|
||||||
packages/fxa-settings/public/locales
|
packages/fxa-settings/public/locales
|
||||||
packages/fxa-settings/legal-docs/
|
packages/fxa-settings/legal-docs/
|
||||||
packages/fxa-settings/public/legal-docs
|
packages/fxa-settings/public/legal-docs
|
||||||
|
|
|
@ -999,7 +999,7 @@ const conf = (module.exports = convict({
|
||||||
},
|
},
|
||||||
l10n: {
|
l10n: {
|
||||||
baseUrl: {
|
baseUrl: {
|
||||||
default: '/settings/locales',
|
default: '/settings/static',
|
||||||
doc: 'The path (or url) where ftl files are held.',
|
doc: 'The path (or url) where ftl files are held.',
|
||||||
env: 'L10N_BASE_URL',
|
env: 'L10N_BASE_URL',
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,6 +26,17 @@ describe('<AppLocalizationProvider/>', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
fetchMock.get(
|
||||||
|
'/static-asset-manifest.json',
|
||||||
|
`
|
||||||
|
{
|
||||||
|
"/locales/en-US/greetings.ftl": "/locales/en-US/greetings.ftl",
|
||||||
|
"/locales/en-US/farewells.ftl": "/locales/en-US/farewells.ftl",
|
||||||
|
"/locales/es-ES/greetings.ftl": "/locales/es-ES/greetings.ftl",
|
||||||
|
"/locales/en-GB/greetings.ftl": "/locales/en-GB/greetings.ftl",
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
fetchMock.get('/locales/en-US/greetings.ftl', 'hello = Hello\n');
|
fetchMock.get('/locales/en-US/greetings.ftl', 'hello = Hello\n');
|
||||||
fetchMock.get('/locales/en-US/farewells.ftl', 'goodbye = Goodbye\n');
|
fetchMock.get('/locales/en-US/farewells.ftl', 'goodbye = Goodbye\n');
|
||||||
fetchMock.get('/locales/es-ES/greetings.ftl', 'hello = Hola\n');
|
fetchMock.get('/locales/es-ES/greetings.ftl', 'hello = Hola\n');
|
||||||
|
|
|
@ -7,9 +7,39 @@ import { LocalizationProvider, ReactLocalization } from '@fluent/react';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { EN_GB_LOCALES, parseAcceptLanguage } from '@fxa/shared/l10n';
|
import { EN_GB_LOCALES, parseAcceptLanguage } from '@fxa/shared/l10n';
|
||||||
|
|
||||||
async function fetchMessages(baseDir: string, locale: string, bundle: string) {
|
/**
|
||||||
|
* Gets l10n messages from server
|
||||||
|
* @param baseDir The root location where locales folders are held
|
||||||
|
* @param locale The target language
|
||||||
|
* @param bundle The target bundle (ie main)
|
||||||
|
* @param mappings A set of mappings for static resources.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async function fetchMessages(
|
||||||
|
baseDir: string,
|
||||||
|
locale: string,
|
||||||
|
bundle: string,
|
||||||
|
mappings?: Record<string, string>
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${baseDir}/${locale}/${bundle}.ftl`);
|
// Build the path to l10n file
|
||||||
|
let path = `locales/${locale}/${bundle}.ftl`;
|
||||||
|
|
||||||
|
// If mappings were proivided see if there is one for the path. This
|
||||||
|
// will be a location where the file path contains a hash in the file
|
||||||
|
// name
|
||||||
|
if (mappings) {
|
||||||
|
path = mappings[path];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have mapped path, there are no l10n resources for this language.
|
||||||
|
if (!path) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the file and return the messages
|
||||||
|
const resolvedPath = `${baseDir}/${path}`;
|
||||||
|
const response = await fetch(resolvedPath);
|
||||||
const messages = await response.text();
|
const messages = await response.text();
|
||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
|
@ -23,23 +53,41 @@ async function fetchMessages(baseDir: string, locale: string, bundle: string) {
|
||||||
function fetchAllMessages(
|
function fetchAllMessages(
|
||||||
baseDir: string,
|
baseDir: string,
|
||||||
locale: string,
|
locale: string,
|
||||||
bundles: Array<string>
|
bundles: Array<string>,
|
||||||
|
mappings?: Record<string, string>
|
||||||
) {
|
) {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
bundles.map((bndl) => fetchMessages(baseDir, locale, bndl))
|
bundles.map((bndl) => fetchMessages(baseDir, locale, bndl, mappings))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchL10nHashedMappings(mappingUrl: string) {
|
||||||
|
try {
|
||||||
|
// These mappigns are currently generated with grunt. See grunt task hash-static
|
||||||
|
// in fxa-settings for an example of how the mappings are generated.
|
||||||
|
const mappingsResponse = await fetch(mappingUrl);
|
||||||
|
const json = await mappingsResponse.json();
|
||||||
|
return json;
|
||||||
|
} catch (err) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createFluentBundleGenerator(
|
async function createFluentBundleGenerator(
|
||||||
baseDir: string,
|
baseDir: string,
|
||||||
currentLocales: Array<string>,
|
currentLocales: Array<string>,
|
||||||
bundles: Array<string>
|
bundles: Array<string>
|
||||||
) {
|
) {
|
||||||
|
const mappings = await fetchL10nHashedMappings(
|
||||||
|
`${baseDir}/static-asset-manifest.json`
|
||||||
|
);
|
||||||
const fetched = await Promise.all(
|
const fetched = await Promise.all(
|
||||||
currentLocales
|
currentLocales
|
||||||
.filter((l) => !EN_GB_LOCALES.includes(l))
|
.filter((l) => !EN_GB_LOCALES.includes(l))
|
||||||
.map(async (locale) => {
|
.map(async (locale) => {
|
||||||
return { [locale]: await fetchAllMessages(baseDir, locale, bundles) };
|
return {
|
||||||
|
[locale]: await fetchAllMessages(baseDir, locale, bundles, mappings),
|
||||||
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -84,7 +132,7 @@ type Props = {
|
||||||
|
|
||||||
export default class AppLocalizationProvider extends Component<Props, State> {
|
export default class AppLocalizationProvider extends Component<Props, State> {
|
||||||
static defaultProps: Props = {
|
static defaultProps: Props = {
|
||||||
baseDir: '/locales',
|
baseDir: '',
|
||||||
userLocales: ['en'],
|
userLocales: ['en'],
|
||||||
bundles: ['main'],
|
bundles: ['main'],
|
||||||
children: React.createElement('div'),
|
children: React.createElement('div'),
|
||||||
|
|
|
@ -37,6 +37,23 @@ module.exports = function (grunt) {
|
||||||
dest: 'test/settings.ftl',
|
dest: 'test/settings.ftl',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hash: {
|
||||||
|
options: {
|
||||||
|
mapping: 'public/static/static-asset-manifest.json', // The file where the hashed file names will be stored
|
||||||
|
srcBasePath: 'public/', // the base Path you want to remove from the `key` string in the mapping file
|
||||||
|
destBasePath: 'public/static',
|
||||||
|
},
|
||||||
|
locales: {
|
||||||
|
expand: true,
|
||||||
|
cwd: 'public/locales',
|
||||||
|
src: '**/*.ftl',
|
||||||
|
dest: 'public/static/locales/',
|
||||||
|
rename: function (dest, src) {
|
||||||
|
const lang = src.split('/')[0];
|
||||||
|
return `${dest}/${lang}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ftl: {
|
ftl: {
|
||||||
files: srcPaths,
|
files: srcPaths,
|
||||||
|
@ -51,8 +68,11 @@ module.exports = function (grunt) {
|
||||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||||
|
grunt.loadNpmTasks('grunt-hash');
|
||||||
|
|
||||||
grunt.registerTask('merge-ftl', ['copy:branding-ftl', 'concat:ftl']);
|
grunt.registerTask('merge-ftl', ['copy:branding-ftl', 'concat:ftl']);
|
||||||
grunt.registerTask('merge-ftl:test', ['concat:ftl-test']);
|
grunt.registerTask('merge-ftl:test', ['concat:ftl-test']);
|
||||||
grunt.registerTask('watch-ftl', ['watch:ftl']);
|
grunt.registerTask('watch-ftl', ['watch:ftl']);
|
||||||
|
|
||||||
|
grunt.registerTask('hash-static', ['hash']);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "nx l10n-prime && nx legal-prime",
|
"prebuild": "nx l10n-prime && nx legal-prime",
|
||||||
"build": "nx build-l10n && nx build-ts && nx build-css && nx build-react",
|
"build": "nx build-l10n && nx build-static && nx build-ts && nx build-css && nx build-react",
|
||||||
|
"build-static": "yarn grunt hash-static",
|
||||||
"build-ts": "tsc --build",
|
"build-ts": "tsc --build",
|
||||||
"build-css": "NODE_ENV=production tailwindcss -i ./src/styles/tailwind.css -o ./src/styles/tailwind.out.css --postcss",
|
"build-css": "NODE_ENV=production tailwindcss -i ./src/styles/tailwind.css -o ./src/styles/tailwind.out.css --postcss",
|
||||||
"build-storybook": "NODE_ENV=production STORYBOOK_BUILD=1 yarn build-css && NODE_OPTIONS=--openssl-legacy-provider sb build && cp -r public/locales ./storybook-static/locales",
|
"build-storybook": "NODE_ENV=production STORYBOOK_BUILD=1 yarn build-css && NODE_OPTIONS=--openssl-legacy-provider sb build && cp -r public/locales ./storybook-static/locales",
|
||||||
|
@ -237,6 +238,7 @@
|
||||||
"grunt-cli": "^1.4.3",
|
"grunt-cli": "^1.4.3",
|
||||||
"grunt-contrib-concat": "^2.1.0",
|
"grunt-contrib-concat": "^2.1.0",
|
||||||
"grunt-contrib-watch": "^1.1.0",
|
"grunt-contrib-watch": "^1.1.0",
|
||||||
|
"grunt-hash": "^0.5.0",
|
||||||
"jest-watch-typeahead": "0.6.5",
|
"jest-watch-typeahead": "0.6.5",
|
||||||
"mutationobserver-shim": "^0.3.7",
|
"mutationobserver-shim": "^0.3.7",
|
||||||
"nx": "18.3.1",
|
"nx": "18.3.1",
|
||||||
|
@ -245,6 +247,7 @@
|
||||||
"postcss-assets": "^6.0.0",
|
"postcss-assets": "^6.0.0",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
|
"raw-loader": "^4.0.2",
|
||||||
"sinon": "^15.0.1",
|
"sinon": "^15.0.1",
|
||||||
"storybook": "^7.0.23",
|
"storybook": "^7.0.23",
|
||||||
"storybook-addon-rtl": "^0.5.0",
|
"storybook-addon-rtl": "^0.5.0",
|
||||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -41625,6 +41625,7 @@ fsevents@~2.1.1:
|
||||||
grunt-cli: ^1.4.3
|
grunt-cli: ^1.4.3
|
||||||
grunt-contrib-concat: ^2.1.0
|
grunt-contrib-concat: ^2.1.0
|
||||||
grunt-contrib-watch: ^1.1.0
|
grunt-contrib-watch: ^1.1.0
|
||||||
|
grunt-hash: ^0.5.0
|
||||||
html-webpack-plugin: ^5.6.0
|
html-webpack-plugin: ^5.6.0
|
||||||
identity-obj-proxy: ^3.0.0
|
identity-obj-proxy: ^3.0.0
|
||||||
jest: ^29.7.0
|
jest: ^29.7.0
|
||||||
|
@ -41643,6 +41644,7 @@ fsevents@~2.1.1:
|
||||||
postcss-normalize: ^10.0.1
|
postcss-normalize: ^10.0.1
|
||||||
postcss-preset-env: ^10.0.5
|
postcss-preset-env: ^10.0.5
|
||||||
prop-types: ^15.8.1
|
prop-types: ^15.8.1
|
||||||
|
raw-loader: ^4.0.2
|
||||||
react-app-polyfill: ^3.0.0
|
react-app-polyfill: ^3.0.0
|
||||||
react-async-hook: ^4.0.0
|
react-async-hook: ^4.0.0
|
||||||
react-dev-utils: ^12.0.1
|
react-dev-utils: ^12.0.1
|
||||||
|
@ -43337,6 +43339,15 @@ fsevents@~2.1.1:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"grunt-hash@npm:^0.5.0":
|
||||||
|
version: 0.5.0
|
||||||
|
resolution: "grunt-hash@npm:0.5.0"
|
||||||
|
bin:
|
||||||
|
grunt-hash: bin/grunt-hash
|
||||||
|
checksum: 2781250dbd476e8302085d816e60a94e0f94f9696e8cd434cecbb66dfc444a561d251409ae88e1f5d82be2996c390aa8681d5fb8f3638ba30e496417e61cf5cd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"grunt-htmllint@npm:0.3.0":
|
"grunt-htmllint@npm:0.3.0":
|
||||||
version: 0.3.0
|
version: 0.3.0
|
||||||
resolution: "grunt-htmllint@npm:0.3.0"
|
resolution: "grunt-htmllint@npm:0.3.0"
|
||||||
|
@ -59754,6 +59765,18 @@ fsevents@~2.1.1:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"raw-loader@npm:^4.0.2":
|
||||||
|
version: 4.0.2
|
||||||
|
resolution: "raw-loader@npm:4.0.2"
|
||||||
|
dependencies:
|
||||||
|
loader-utils: ^2.0.0
|
||||||
|
schema-utils: ^3.0.0
|
||||||
|
peerDependencies:
|
||||||
|
webpack: ^4.0.0 || ^5.0.0
|
||||||
|
checksum: 51cc1b0d0e8c37c4336b5318f3b2c9c51d6998ad6f56ea09612afcfefc9c1f596341309e934a744ae907177f28efc9f1654eacd62151e82853fcc6d37450e795
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"rc@npm:^1.2.7":
|
"rc@npm:^1.2.7":
|
||||||
version: 1.2.8
|
version: 1.2.8
|
||||||
resolution: "rc@npm:1.2.8"
|
resolution: "rc@npm:1.2.8"
|
||||||
|
|
Загрузка…
Ссылка в новой задаче