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:
dschom 2024-10-02 10:14:47 -07:00
Родитель 5d1652341a
Коммит 914afe5343
Не найден ключ, соответствующий данной подписи
7 изменённых файлов: 114 добавлений и 8 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -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",

Просмотреть файл

@ -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"